@jgardner04/ghost-mcp-server 1.0.0 → 1.1.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.
@@ -1,11 +1,8 @@
1
1
  import {
2
2
  ErrorHandler,
3
- BaseError,
4
3
  ValidationError,
5
- NotFoundError,
6
4
  AuthenticationError,
7
- AuthorizationError,
8
- RateLimitError
5
+ RateLimitError,
9
6
  } from '../errors/index.js';
10
7
  import fs from 'fs/promises';
11
8
  import path from 'path';
@@ -23,7 +20,7 @@ export class ErrorLogger {
23
20
  this.maxLogSize = options.maxLogSize || 10 * 1024 * 1024; // 10MB
24
21
  this.logLevel = options.logLevel || process.env.LOG_LEVEL || 'info';
25
22
  this.enableFileLogging = options.enableFileLogging ?? true;
26
-
23
+
27
24
  // Ensure log directory exists
28
25
  if (this.enableFileLogging) {
29
26
  this.ensureLogDirectory();
@@ -52,27 +49,29 @@ export class ErrorLogger {
52
49
  const rotatedPath = filePath.replace('.log', `-${timestamp}.log`);
53
50
  await fs.rename(filePath, rotatedPath);
54
51
  }
55
- } catch (error) {
52
+ } catch (_error) {
56
53
  // File doesn't exist yet, which is fine
57
54
  }
58
55
  }
59
56
 
60
57
  formatLogEntry(level, message, meta = {}) {
61
- return JSON.stringify({
62
- timestamp: new Date().toISOString(),
63
- level,
64
- message,
65
- ...meta,
66
- environment: process.env.NODE_ENV || 'development',
67
- pid: process.pid
68
- }) + '\n';
58
+ return (
59
+ JSON.stringify({
60
+ timestamp: new Date().toISOString(),
61
+ level,
62
+ message,
63
+ ...meta,
64
+ environment: process.env.NODE_ENV || 'development',
65
+ pid: process.pid,
66
+ }) + '\n'
67
+ );
69
68
  }
70
69
 
71
70
  async writeToFile(type, entry) {
72
71
  if (!this.enableFileLogging) return;
73
-
72
+
74
73
  const filePath = this.getLogFilePath(type);
75
-
74
+
76
75
  try {
77
76
  await this.rotateLogIfNeeded(filePath);
78
77
  await fs.appendFile(filePath, entry);
@@ -84,7 +83,7 @@ export class ErrorLogger {
84
83
  async logError(error, context = {}) {
85
84
  const isOperational = ErrorHandler.isOperationalError(error);
86
85
  const level = isOperational ? 'error' : 'fatal';
87
-
86
+
88
87
  const logData = {
89
88
  name: error.name || 'Error',
90
89
  message: error.message,
@@ -92,7 +91,7 @@ export class ErrorLogger {
92
91
  statusCode: error.statusCode,
93
92
  stack: error.stack,
94
93
  isOperational,
95
- ...context
94
+ ...context,
96
95
  };
97
96
 
98
97
  // Console logging
@@ -145,21 +144,22 @@ export class ErrorMetrics {
145
144
  errorsByType: {},
146
145
  errorsByStatusCode: {},
147
146
  errorsByEndpoint: {},
148
- lastReset: new Date().toISOString()
147
+ lastReset: new Date().toISOString(),
149
148
  };
150
149
  }
151
150
 
152
151
  recordError(error, endpoint = null) {
153
152
  this.metrics.totalErrors++;
154
-
153
+
155
154
  // Count by error type
156
155
  const errorType = error.constructor.name;
157
156
  this.metrics.errorsByType[errorType] = (this.metrics.errorsByType[errorType] || 0) + 1;
158
-
157
+
159
158
  // Count by status code
160
159
  const statusCode = error.statusCode || 500;
161
- this.metrics.errorsByStatusCode[statusCode] = (this.metrics.errorsByStatusCode[statusCode] || 0) + 1;
162
-
160
+ this.metrics.errorsByStatusCode[statusCode] =
161
+ (this.metrics.errorsByStatusCode[statusCode] || 0) + 1;
162
+
163
163
  // Count by endpoint
164
164
  if (endpoint) {
165
165
  this.metrics.errorsByEndpoint[endpoint] = (this.metrics.errorsByEndpoint[endpoint] || 0) + 1;
@@ -171,7 +171,7 @@ export class ErrorMetrics {
171
171
  ...this.metrics,
172
172
  uptime: process.uptime(),
173
173
  memoryUsage: process.memoryUsage(),
174
- timestamp: new Date().toISOString()
174
+ timestamp: new Date().toISOString(),
175
175
  };
176
176
  }
177
177
 
@@ -181,7 +181,7 @@ export class ErrorMetrics {
181
181
  errorsByType: {},
182
182
  errorsByStatusCode: {},
183
183
  errorsByEndpoint: {},
184
- lastReset: new Date().toISOString()
184
+ lastReset: new Date().toISOString(),
185
185
  };
186
186
  }
187
187
  }
@@ -193,13 +193,13 @@ const errorMetrics = new ErrorMetrics();
193
193
  /**
194
194
  * Express Error Middleware
195
195
  */
196
- export function expressErrorHandler(err, req, res, next) {
196
+ export function expressErrorHandler(err, req, res, _next) {
197
197
  // Log the error
198
198
  errorLogger.logError(err, {
199
199
  method: req.method,
200
200
  url: req.url,
201
201
  ip: req.ip,
202
- userAgent: req.get('user-agent')
202
+ userAgent: req.get('user-agent'),
203
203
  });
204
204
 
205
205
  // Record metrics
@@ -207,12 +207,12 @@ export function expressErrorHandler(err, req, res, next) {
207
207
 
208
208
  // Format response
209
209
  const { statusCode, body } = ErrorHandler.formatHTTPError(err);
210
-
210
+
211
211
  // Set security headers
212
212
  res.set({
213
213
  'X-Content-Type-Options': 'nosniff',
214
214
  'X-Frame-Options': 'DENY',
215
- 'X-XSS-Protection': '1; mode=block'
215
+ 'X-XSS-Protection': '1; mode=block',
216
216
  });
217
217
 
218
218
  // Send response
@@ -241,13 +241,15 @@ export function validateRequest(schema) {
241
241
  if (validationResult && validationResult.error) {
242
242
  throw new ValidationError(validationResult.error);
243
243
  }
244
- }
244
+ }
245
245
  // If schema has a validate method (e.g., Joi schema)
246
246
  else if (schema && typeof schema.validate === 'function') {
247
247
  const { error } = schema.validate(req.body, { abortEarly: false });
248
248
  if (error) {
249
249
  // Create ValidationError from Joi error details
250
- const errors = error.details ? error.details.map(detail => detail.message) : [error.message];
250
+ const errors = error.details
251
+ ? error.details.map((detail) => detail.message)
252
+ : [error.message];
251
253
  throw new ValidationError('Validation failed', errors);
252
254
  }
253
255
  }
@@ -266,7 +268,7 @@ export function validateRequest(schema) {
266
268
  throw new ValidationError('Validation failed', errors);
267
269
  }
268
270
  }
269
-
271
+
270
272
  next();
271
273
  } catch (error) {
272
274
  next(error);
@@ -288,33 +290,33 @@ export class RateLimiter {
288
290
  return (req, res, next) => {
289
291
  const key = req.ip;
290
292
  const now = Date.now();
291
-
293
+
292
294
  // Clean old entries
293
295
  this.cleanup(now);
294
-
296
+
295
297
  // Get or create request list for this IP
296
298
  if (!this.requests.has(key)) {
297
299
  this.requests.set(key, []);
298
300
  }
299
-
301
+
300
302
  const requestTimes = this.requests.get(key);
301
303
  requestTimes.push(now);
302
-
304
+
303
305
  if (requestTimes.length > this.maxRequests) {
304
306
  const retryAfter = Math.ceil((this.windowMs - (now - requestTimes[0])) / 1000);
305
307
  return next(new RateLimitError(retryAfter));
306
308
  }
307
-
309
+
308
310
  next();
309
311
  };
310
312
  }
311
313
 
312
314
  cleanup(now) {
313
315
  const cutoff = now - this.windowMs;
314
-
316
+
315
317
  for (const [key, times] of this.requests.entries()) {
316
- const filtered = times.filter(time => time > cutoff);
317
-
318
+ const filtered = times.filter((time) => time > cutoff);
319
+
318
320
  if (filtered.length === 0) {
319
321
  this.requests.delete(key);
320
322
  } else {
@@ -329,29 +331,29 @@ export class RateLimiter {
329
331
  */
330
332
  export function apiKeyAuth(apiKey) {
331
333
  return (req, res, next) => {
332
- const providedKey = req.headers['x-api-key'] ||
333
- req.headers['authorization']?.replace('Bearer ', '');
334
-
334
+ const providedKey =
335
+ req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', '');
336
+
335
337
  if (!providedKey) {
336
338
  return next(new AuthenticationError('API key is required'));
337
339
  }
338
-
340
+
339
341
  // Use timing-safe comparison to prevent timing attacks
340
342
  const expectedKeyBuffer = Buffer.from(apiKey, 'utf8');
341
343
  const providedKeyBuffer = Buffer.from(providedKey, 'utf8');
342
-
344
+
343
345
  // Ensure buffers are same length to prevent timing attacks
344
346
  if (expectedKeyBuffer.length !== providedKeyBuffer.length) {
345
347
  return next(new AuthenticationError('Invalid API key'));
346
348
  }
347
-
349
+
348
350
  // Use constant-time comparison
349
351
  const isValid = crypto.timingSafeEqual(expectedKeyBuffer, providedKeyBuffer);
350
-
352
+
351
353
  if (!isValid) {
352
354
  return next(new AuthenticationError('Invalid API key'));
353
355
  }
354
-
356
+
355
357
  next();
356
358
  };
357
359
  }
@@ -362,18 +364,18 @@ export function apiKeyAuth(apiKey) {
362
364
  export function mcpCors(allowedOrigins = ['*']) {
363
365
  return (req, res, next) => {
364
366
  const origin = req.headers.origin;
365
-
367
+
366
368
  if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) {
367
369
  res.header('Access-Control-Allow-Origin', origin || '*');
368
370
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
369
371
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
370
372
  res.header('Access-Control-Max-Age', '86400');
371
373
  }
372
-
374
+
373
375
  if (req.method === 'OPTIONS') {
374
376
  return res.sendStatus(204);
375
377
  }
376
-
378
+
377
379
  next();
378
380
  };
379
381
  }
@@ -386,22 +388,22 @@ export function healthCheck(ghostService) {
386
388
  try {
387
389
  const health = await ghostService.checkHealth();
388
390
  const metrics = errorMetrics.getMetrics();
389
-
391
+
390
392
  const status = health.status === 'healthy' ? 200 : 503;
391
-
393
+
392
394
  res.status(status).json({
393
395
  ...health,
394
396
  metrics: {
395
397
  errors: metrics.totalErrors,
396
398
  uptime: metrics.uptime,
397
- memory: metrics.memoryUsage
398
- }
399
+ memory: metrics.memoryUsage,
400
+ },
399
401
  });
400
402
  } catch (error) {
401
403
  res.status(503).json({
402
404
  status: 'unhealthy',
403
405
  error: error.message,
404
- timestamp: new Date().toISOString()
406
+ timestamp: new Date().toISOString(),
405
407
  });
406
408
  }
407
409
  };
@@ -428,12 +430,12 @@ export class GracefulShutdown {
428
430
  res.status(503).json({
429
431
  error: {
430
432
  code: 'SERVER_SHUTTING_DOWN',
431
- message: 'Server is shutting down'
432
- }
433
+ message: 'Server is shutting down',
434
+ },
433
435
  });
434
436
  return;
435
437
  }
436
-
438
+
437
439
  // Track the connection
438
440
  this.trackConnection(req.socket);
439
441
  next();
@@ -442,36 +444,33 @@ export class GracefulShutdown {
442
444
 
443
445
  async shutdown(server) {
444
446
  if (this.isShuttingDown) return;
445
-
447
+
446
448
  this.isShuttingDown = true;
447
449
  console.log('Graceful shutdown initiated...');
448
-
450
+
449
451
  // Stop accepting new connections
450
452
  server.close(() => {
451
453
  console.log('Server closed to new connections');
452
454
  });
453
-
455
+
454
456
  // Close existing connections
455
457
  for (const connection of this.connections) {
456
458
  connection.end();
457
459
  }
458
-
460
+
459
461
  // Force close after timeout
460
462
  setTimeout(() => {
461
463
  for (const connection of this.connections) {
462
464
  connection.destroy();
463
465
  }
464
466
  }, 10000);
465
-
467
+
466
468
  // Log final metrics
467
469
  await errorLogger.logInfo('Shutdown metrics', errorMetrics.getMetrics());
468
470
  }
469
471
  }
470
472
 
471
- export {
472
- errorLogger,
473
- errorMetrics
474
- };
473
+ export { errorLogger, errorMetrics };
475
474
 
476
475
  export default {
477
476
  expressErrorHandler,
@@ -485,5 +484,5 @@ export default {
485
484
  ErrorLogger,
486
485
  ErrorMetrics,
487
486
  errorLogger,
488
- errorMetrics
489
- };
487
+ errorMetrics,
488
+ };