@manojkmfsi/monodog 1.0.20 → 1.0.21

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>MonoDog Dashboard</title>
7
- <script type="module" crossorigin src="/assets/index-dadb5f0d.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-1a6836e4.js"></script>
8
8
  <link rel="stylesheet" href="/assets/index-504dc418.css">
9
9
  </head>
10
10
  <body class="bg-gray-100 text-gray-900">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manojkmfsi/monodog",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "App for monodog monorepo",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -11,7 +11,9 @@
11
11
  "body-parser": "^1.20.4",
12
12
  "cors": "^2.8.5",
13
13
  "express": "^4.22.1",
14
+ "helmet": "^7.1.0",
14
15
  "init": "^0.1.2",
16
+ "js-yaml": "^4.1.0",
15
17
  "prisma": "^5.22.0"
16
18
  },
17
19
  "devDependencies": {
@@ -19,6 +21,7 @@
19
21
  "@types/cors": "^2.8.19",
20
22
  "@types/express": "^4.17.25",
21
23
  "@types/jest": "^29.5.14",
24
+ "@types/js-yaml": "^4.0.9",
22
25
  "@types/node": "^20.19.27",
23
26
  "cross-env": "^10.1.0",
24
27
  "jest": "^29.7.0",
@@ -28,8 +31,6 @@
28
31
  "tsx": "^4.21.0",
29
32
  "typescript": "^5.9.3"
30
33
  },
31
- "repository": {},
32
- "peerDependencies": {},
33
34
  "scripts": {
34
35
  "dev": "DATABASE_URL=$(npm run db:url --silent 2>/dev/null | tr -d '\\n') tsx watch src/serve.js",
35
36
  "serve": "DATABASE_URL=$(npm run db:url --silent 2>/dev/null | tr -d '\\n') tsx dist/serve.js",
@@ -25,7 +25,7 @@ interface MonodogConfig {
25
25
  let config: MonodogConfig | null = null;
26
26
 
27
27
  /**
28
- * Loads the monodog-conf.json file from the monorepo root.
28
+ * Loads the monodog-config.json file from the monorepo root.
29
29
  * This should be called only once during application startup.
30
30
  * @returns The application configuration object.
31
31
  */
@@ -37,8 +37,8 @@ function loadConfig(): MonodogConfig {
37
37
  // 1. Determine the path to the config file
38
38
  // We assume the backend package is running from the monorepo root (cwd is root)
39
39
  // or that we can navigate up to the root from the current file's location.
40
- const rootPath = path.resolve(process.cwd()); // Adjust based on your workspace folder depth from root if needed
41
- const configPath = path.resolve(rootPath, 'monodog-conf.json');
40
+ const rootPath = path.resolve(process.cwd());
41
+ const configPath = path.resolve(rootPath, 'monodog-config.json');
42
42
  createConfigFileIfMissing(rootPath);
43
43
 
44
44
  if (!fs.existsSync(configPath)) {
@@ -51,14 +51,12 @@ function loadConfig(): MonodogConfig {
51
51
  const fileContent = fs.readFileSync(configPath, 'utf-8');
52
52
  const parsedConfig = JSON.parse(fileContent) as MonodogConfig;
53
53
 
54
- // 3. Optional: Add validation logic here (e.g., check if ports are numbers)
55
-
56
54
  // Cache and return
57
55
  config = parsedConfig;
58
56
  process.stderr.write('[Config] Loaded configuration from: ...\n');
59
57
  return config;
60
58
  } catch (error) {
61
- console.error('ERROR: Failed to read or parse monodog-conf.json.');
59
+ console.error('ERROR: Failed to read or parse monodog-config.json.');
62
60
  console.error(error);
63
61
  process.exit(1);
64
62
  }
@@ -66,7 +64,7 @@ function loadConfig(): MonodogConfig {
66
64
 
67
65
  function createConfigFileIfMissing(rootPath: string): void {
68
66
  // --- CONFIGURATION ---
69
- const configFileName = 'monodog-conf.json';
67
+ const configFileName = 'monodog-config.json';
70
68
  const configFilePath = path.resolve(rootPath, configFileName);
71
69
 
72
70
  // The default content for the configuration file
package/src/get-db-url.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { appConfig } from './config-loader';
2
2
 
3
3
  function generateUrl() {
4
- // const appConfig = loadConfig();
5
-
6
4
  const DATABASE_URL = `${appConfig.database.path}`;
7
5
  process.env.DATABASE_URL = DATABASE_URL;
8
6
  process.stdout.write(DATABASE_URL);
package/src/index.ts CHANGED
@@ -2,10 +2,13 @@ import express, {
2
2
  type Request,
3
3
  type Response,
4
4
  type NextFunction,
5
+ type ErrorRequestHandler,
5
6
  } from 'express';
7
+
6
8
  import cors from 'cors';
7
9
  import path from 'path';
8
10
  import { json } from 'body-parser';
11
+ import helmet from 'helmet';
9
12
 
10
13
  import { appConfig } from './config-loader';
11
14
 
@@ -13,52 +16,116 @@ import packageRouter from './routes/packageRoutes';
13
16
  import commitRouter from './routes/commitRoutes';
14
17
  import healthRouter from './routes/healthRoutes';
15
18
  import configRouter from './routes/configRoutes';
19
+
20
+ // Security constants
21
+ const PORT_MIN = 1024;
22
+ const PORT_MAX = 65535;
23
+
24
+ // Validate port number
25
+ function validatePort(port: string | number): number {
26
+ const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
27
+
28
+ if (isNaN(portNum) || portNum < PORT_MIN || portNum > PORT_MAX) {
29
+ throw new Error(`Port must be between ${PORT_MIN} and ${PORT_MAX}`);
30
+ }
31
+
32
+ return portNum;
33
+ }
34
+
35
+ // Global error handler
36
+ const errorHandler: ErrorRequestHandler = (err: any, req: Request, res: Response, _next: NextFunction) => {
37
+ const status = err.status || err.statusCode || 500;
38
+ const message = process.env.NODE_ENV === 'production'
39
+ ? 'Internal server error'
40
+ : err.message;
41
+
42
+ console.error('[ERROR]', {
43
+ status,
44
+ method: req.method,
45
+ path: req.path,
46
+ message: err.message,
47
+ });
48
+
49
+ res.status(status).json({
50
+ error: 'Internal server error'
51
+ });
52
+ };
53
+
16
54
  // The main function exported and called by the CLI
17
55
  export function startServer(
18
- rootPath: string,
19
- port: number | string,
20
- host: string
56
+ rootPath: string
21
57
  ): void {
22
- const app = express();
23
- app.locals.rootPath = rootPath;
24
- // --- Middleware ---
25
-
26
- // 1. Logging Middleware
27
- app.use((_req: Request, _res: Response, next: NextFunction) => {
28
- console.log(`[SERVER] ${_req.method} ${_req.url} (Root: ${rootPath})`);
29
- next();
30
- });
31
- app.use(cors());
32
- app.use(json());
33
58
 
59
+ try {
60
+ const port = appConfig.server.port;
61
+ const host = appConfig.server.host;
62
+ const validatedPort = validatePort(port);
63
+ const app = express();
34
64
 
35
- app.use('/api/packages', packageRouter);
65
+ // Set request timeout (30 seconds)
66
+ app.use((req, res, next) => {
67
+ req.setTimeout(30000);
68
+ res.setTimeout(30000);
69
+ next();
70
+ });
36
71
 
72
+ app.locals.rootPath = rootPath;
37
73
 
38
- // Get commit details
39
- app.use('/api/commits/', commitRouter);
74
+ // Security middleware with CSP allowing API calls
75
+ const apiHost = host === '0.0.0.0' ? 'localhost' : host;
76
+ const apiUrl = process.env.API_URL || `http://${apiHost}:${validatedPort}`;
77
+ const dashboardHost = appConfig.dashboard.host === '0.0.0.0' ? 'localhost' : appConfig.dashboard.host;
78
+ const dashboardUrl = `http://${dashboardHost}:${appConfig.dashboard.port}`;
40
79
 
41
- // ---------- HEALTH --------------------
80
+ app.use(helmet({
81
+ contentSecurityPolicy: {
82
+ directives: {
83
+ defaultSrc: ["'self'"],
84
+ connectSrc: ["'self'", apiUrl, 'http://localhost:*', 'http://127.0.0.1:*'],
85
+ scriptSrc: ["'self'"],
86
+ imgSrc: ["'self'", 'data:', 'https:'],
87
+ },
88
+ },
89
+ }));
90
+ app.use(cors({
91
+ origin: process.env.CORS_ORIGIN || dashboardUrl,
92
+ credentials: true,
93
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
94
+ allowedHeaders: ['Content-Type', 'Authorization'],
95
+ }));
42
96
 
43
- app.use('/api/health/', healthRouter)
97
+ app.use(json({ limit: '1mb' }));
44
98
 
45
- // ------------------------- CONFIGURATION TAB ------------------------- //
46
- // Get all configuration files from the file system
47
- app.use('/api/config/', configRouter);
99
+ // Request logging middleware (safe version)
100
+ app.use((_req: Request, _res: Response, next: NextFunction) => {
101
+ console.log(`[${new Date().toISOString()}] ${_req.method} ${_req.path}`);
102
+ next();
103
+ });
104
+
105
+ app.use('/api/packages', packageRouter);
106
+
107
+ // Get commit details
108
+ app.use('/api/commits/', commitRouter);
48
109
 
49
- // 404 handler
50
- app.use('*', (_, res) => {
51
- res.status(404).json({
52
- error: 'Endpoint not found',
53
- timestamp: Date.now(),
110
+ // Health check endpoint
111
+ app.use('/api/health/', healthRouter);
112
+
113
+ // Configuration endpoint
114
+ app.use('/api/config/', configRouter);
115
+
116
+ // 404 handler
117
+ app.use('*', (_, res) => {
118
+ res.status(404).json({
119
+ error: 'Endpoint not found',
120
+ timestamp: Date.now(),
121
+ });
54
122
  });
55
- });
56
123
 
57
- const PORT = parseInt(port ? port.toString() : '4000');
124
+ // Global error handler (must be last)
125
+ app.use(errorHandler);
58
126
 
59
- app
60
- .listen(PORT, host, async () => {
61
- console.log(`🚀 Backend server running on http://${host}:${PORT}`);
127
+ const server = app.listen(validatedPort, host, () => {
128
+ console.log(`🚀 Backend server running on http://${host}:${validatedPort}`);
62
129
  console.log(`📊 API endpoints available:`);
63
130
  console.log(` - GET /api/health`);
64
131
  console.log(` - GET /api/packages/refresh`);
@@ -69,61 +136,149 @@ app.locals.rootPath = rootPath;
69
136
  console.log(` - GET /api/health/packages`);
70
137
  console.log(` - PUT /api/config/files/:id`);
71
138
  console.log(` - GET /api/config/files`);
139
+ });
72
140
 
73
- })
74
- .on('error', err => {
141
+ server.on('error', (err: any) => {
75
142
  // Handle common errors like EADDRINUSE (port already in use)
76
- if (err.message.includes('EADDRINUSE')) {
143
+ if (err.code === 'EADDRINUSE') {
77
144
  console.error(
78
- `Error: Port ${port} is already in use. Please specify a different port via configuration file.`
145
+ `Error: Port ${validatedPort} is already in use. Please specify a different port.`
146
+ );
147
+ process.exit(1);
148
+ } else if (err.code === 'EACCES') {
149
+ console.error(
150
+ `Error: Permission denied to listen on port ${validatedPort}. Use a port above 1024.`
79
151
  );
80
152
  process.exit(1);
81
153
  } else {
82
- console.error('Server failed to start:', err);
154
+ console.error('Server failed to start:', err.message);
83
155
  process.exit(1);
84
156
  }
85
157
  });
158
+
159
+ // Graceful shutdown
160
+ process.on('SIGTERM', () => {
161
+ console.log('SIGTERM signal received: closing HTTP server');
162
+ server.close(() => {
163
+ console.log('HTTP server closed');
164
+ process.exit(0);
165
+ });
166
+ });
167
+ } catch (error: any) {
168
+ console.error('Failed to start server:', error.message);
169
+ process.exit(1);
170
+ }
86
171
  }
87
172
 
88
173
  export function serveDashboard(
89
- rootPath: string,
90
- port: number | string,
91
- host: string
174
+ rootPath: string
92
175
  ): void {
93
- const app = express();
94
- app.get('/env-config.js', (req, res) => {
95
- res.setHeader('Content-Type', 'application/javascript');
96
- res.send(
97
- `window.ENV = { API_URL: "${`${appConfig.server.host}:${appConfig.server.port}` || 'localhost:8999'}" };`
98
- );
99
- });
176
+ try {
177
+ const port = appConfig.dashboard.port;
178
+ const host = appConfig.dashboard.host;
179
+ const validatedPort = validatePort(port);
180
+ const app = express();
181
+
182
+ // Security middleware
183
+ const serverHost = appConfig.server.host === '0.0.0.0' ? 'localhost' : appConfig.server.host;
184
+ const apiUrl = process.env.API_URL || `http://${serverHost}:${appConfig.server.port}`;
185
+ app.use(helmet({
186
+ contentSecurityPolicy: {
187
+ directives: {
188
+ defaultSrc: ["'self'"],
189
+ connectSrc: ["'self'", apiUrl, 'http://localhost:*', 'http://127.0.0.1:*'],
190
+ scriptSrc: ["'self'"],
191
+ imgSrc: ["'self'", 'data:', 'https:'],
192
+ },
193
+ },
194
+ }));
195
+
196
+ // Strict CORS for dashboard
197
+ app.use(cors({
198
+ origin: false, // Don't allow any origin for static assets
199
+ }));
100
200
 
101
- // This code makes sure that any request that does not matches a static file
102
- // in the build folder, will just serve index.html. Client side routing is
103
- // going to make sure that the correct content will be loaded.
104
- app.use((req, res, next) => {
105
- if (/(.ico|.js|.css|.jpg|.png|.map)$/i.test(req.path)) {
201
+ // Set request timeout
202
+ app.use((req, res, next) => {
203
+ req.setTimeout(30000);
204
+ res.setTimeout(30000);
106
205
  next();
107
- } else {
108
- res.header(
109
- 'Cache-Control',
110
- 'private, no-cache, no-store, must-revalidate'
206
+ });
207
+
208
+ app.get('/env-config.js', (req, res) => {
209
+ res.setHeader('Content-Type', 'application/javascript');
210
+ res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
211
+
212
+ const serverHost = appConfig.server.host === '0.0.0.0' ? 'localhost' : appConfig.server.host;
213
+ const apiUrl = process.env.API_URL || `http://${serverHost}:${appConfig.server.port}`;
214
+ res.send(
215
+ `window.ENV = { API_URL: "${apiUrl}" };`
111
216
  );
112
- res.header('Expires', '-1');
113
- res.header('Pragma', 'no-cache');
114
- res.sendFile('index.html', {
115
- root: path.resolve(__dirname, '..', 'monodog-dashboard', 'dist'),
217
+ });
218
+
219
+ // This code makes sure that any request that does not matches a static file
220
+ // in the build folder, will just serve index.html. Client side routing is
221
+ // going to make sure that the correct content will be loaded.
222
+ app.use((req, res, next) => {
223
+ if (/(.ico|.js|.css|.jpg|.png|.map|.woff|.woff2|.ttf)$/i.test(req.path)) {
224
+ next();
225
+ } else {
226
+ res.header(
227
+ 'Cache-Control',
228
+ 'private, no-cache, no-store, must-revalidate'
229
+ );
230
+ res.header('Expires', '-1');
231
+ res.header('Pragma', 'no-cache');
232
+ res.sendFile('index.html', {
233
+ root: path.resolve(__dirname, '..', 'monodog-dashboard', 'dist'),
234
+ }, (err) => {
235
+ if (err) {
236
+ console.error('Error serving index.html:', err.message);
237
+ res.status(500).json({ error: 'Internal server error' });
238
+ }
239
+ });
240
+ }
241
+ });
242
+
243
+ const staticPath = path.join(__dirname, '..', 'monodog-dashboard', 'dist');
244
+ console.log('Serving static files from:', staticPath);
245
+ app.use(express.static(staticPath, {
246
+ maxAge: '1d',
247
+ etag: false,
248
+ dotfiles: 'deny', // Don't serve dot files
249
+ }));
250
+
251
+ // Global error handler
252
+ app.use(errorHandler);
253
+
254
+ const server = app.listen(validatedPort, host, () => {
255
+ console.log(`✅ Dashboard listening on http://${host}:${validatedPort}`);
256
+ console.log('Press Ctrl+C to quit.');
257
+ });
258
+
259
+ server.on('error', (err: any) => {
260
+ if (err.code === 'EADDRINUSE') {
261
+ console.error(`Error: Port ${validatedPort} is already in use.`);
262
+ process.exit(1);
263
+ } else if (err.code === 'EACCES') {
264
+ console.error(`Error: Permission denied to listen on port ${validatedPort}.`);
265
+ process.exit(1);
266
+ } else {
267
+ console.error('Server failed to start:', err.message);
268
+ process.exit(1);
269
+ }
270
+ });
271
+
272
+ // Graceful shutdown
273
+ process.on('SIGTERM', () => {
274
+ console.log('SIGTERM signal received: closing dashboard server');
275
+ server.close(() => {
276
+ console.log('Dashboard server closed');
277
+ process.exit(0);
116
278
  });
117
- }
118
- });
119
- const staticPath = path.join(__dirname, '..', 'monodog-dashboard', 'dist');
120
- console.log('Serving static files from:', staticPath);
121
- app.use(express.static(staticPath));
122
- // Start the server
123
- const PORT = parseInt(port ? port.toString() : '8999');
124
-
125
- app.listen(PORT, host, () => {
126
- console.log(`App listening on ${host}:${port}`);
127
- console.log('Press Ctrl+C to quit.');
128
- });
279
+ });
280
+ } catch (error: any) {
281
+ console.error('Failed to start dashboard:', error.message);
282
+ process.exit(1);
283
+ }
129
284
  }
package/src/serve.ts CHANGED
@@ -2,86 +2,24 @@
2
2
 
3
3
  /**
4
4
  * CLI Entry Point for serving Monodog.
5
- * * This script is executed when a user runs the serve command
5
+ * This script is executed when a user runs the serve command
6
6
  * in their project. It handles command-line arguments to determine
7
7
  * whether to:
8
8
  * 1. Start the API server for the dashboard.
9
9
  * 2. Start serving the dashboard frontend.
10
10
  */
11
11
 
12
- import * as path from 'path';
13
- import { startServer, serveDashboard } from './index'; // Assume index.ts exports this function
12
+ import { startServer, serveDashboard } from './index';
13
+ import { findMonorepoRoot } from './utils/utilities';
14
14
 
15
- import { appConfig } from './config-loader';
16
- import fs from 'fs';
17
-
18
- // --- Argument Parsing ---
19
-
20
- // 1. Get arguments excluding the node executable and script name
21
- const args = process.argv.slice(2);
22
-
23
- // Default settings
24
- const DEFAULT_PORT = 8999;
25
15
  const rootPath = findMonorepoRoot();
26
- const port = appConfig.server.port ?? DEFAULT_PORT; //Default port
27
- const host = appConfig.server.host ?? 'localhost'; //Default host
28
-
29
-
30
- // --- Execution Logic ---
31
16
 
32
17
  console.log(`Starting Monodog API server...`);
33
18
  console.log(`Analyzing monorepo at root: ${rootPath}`);
34
- // Start the Express server and begin analysis
35
- startServer(rootPath, port, host);
36
- serveDashboard(
37
- path.join(rootPath),
38
- appConfig.dashboard.port,
39
- appConfig.dashboard.host
40
- );
41
-
42
- /**
43
- * Find the monorepo root by looking for package.json with workspaces or pnpm-workspace.yaml
44
- */
45
- function findMonorepoRoot(): string {
46
- let currentDir = __dirname;
47
-
48
- while (currentDir !== path.parse(currentDir).root) {
49
- const packageJsonPath = path.join(currentDir, 'package.json');
50
- const pnpmWorkspacePath = path.join(currentDir, 'pnpm-workspace.yaml');
51
19
 
52
- // Check if this directory has package.json with workspaces or pnpm-workspace.yaml
53
- if (fs.existsSync(packageJsonPath)) {
54
- try {
55
- const packageJson = JSON.parse(
56
- fs.readFileSync(packageJsonPath, 'utf8')
57
- );
58
- // If it has workspaces or is the root monorepo package
59
- if (packageJson.workspaces || fs.existsSync(pnpmWorkspacePath)) {
60
- console.log('✅ Found monorepo root:', currentDir);
61
- return currentDir;
62
- }
63
- } catch (error) {
64
- // Continue searching if package.json is invalid
65
- }
66
- }
20
+ // Start the Express server and dashboard
67
21
 
68
- // Check if we're at the git root
69
- const gitPath = path.join(currentDir, '.git');
70
- if (fs.existsSync(gitPath)) {
71
- console.log('✅ Found git root (likely monorepo root):', currentDir);
72
- return currentDir;
73
- }
22
+ startServer(rootPath);
74
23
 
75
- // Go up one directory
76
- const parentDir = path.dirname(currentDir);
77
- if (parentDir === currentDir) break; // Prevent infinite loop
78
- currentDir = parentDir;
79
- }
24
+ serveDashboard(rootPath);
80
25
 
81
- // Fallback to process.cwd() if we can't find the root
82
- console.log(
83
- '⚠️ Could not find monorepo root, using process.cwd():',
84
- process.cwd()
85
- );
86
- return process.cwd();
87
- }
@@ -1,4 +1,4 @@
1
- import { GitService } from '../gitService';
1
+ import { GitService } from './gitService';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
4