@dynamicu/chromedebug-mcp 2.2.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/CLAUDE.md +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- package/src/validation/schemas.js +391 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import bcrypt from 'bcrypt';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
// Configuration
|
|
12
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'chrome-pilot-secret-key-change-in-production';
|
|
13
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
|
|
14
|
+
const API_KEYS_FILE = path.join(__dirname, '../../config/api-keys.json');
|
|
15
|
+
|
|
16
|
+
// Ensure config directory exists
|
|
17
|
+
const configDir = path.dirname(API_KEYS_FILE);
|
|
18
|
+
if (!fs.existsSync(configDir)) {
|
|
19
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Load or initialize API keys
|
|
23
|
+
let apiKeys = [];
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(API_KEYS_FILE)) {
|
|
26
|
+
apiKeys = JSON.parse(fs.readFileSync(API_KEYS_FILE, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Error loading API keys:', error);
|
|
30
|
+
apiKeys = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Save API keys to file
|
|
34
|
+
function saveApiKeys() {
|
|
35
|
+
try {
|
|
36
|
+
fs.writeFileSync(API_KEYS_FILE, JSON.stringify(apiKeys, null, 2));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error saving API keys:', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Roles and permissions
|
|
43
|
+
const ROLES = {
|
|
44
|
+
ADMIN: 'admin',
|
|
45
|
+
USER: 'user',
|
|
46
|
+
READONLY: 'readonly'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// MCP Service roles for internal service-to-service communication
|
|
50
|
+
const MCP_SERVICE_ROLES = {
|
|
51
|
+
MCP_CLIENT: 'mcp_client'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PERMISSIONS = {
|
|
55
|
+
CHROME_CONTROL: 'chrome:control',
|
|
56
|
+
WORKFLOW_READ: 'workflow:read',
|
|
57
|
+
WORKFLOW_WRITE: 'workflow:write',
|
|
58
|
+
WORKFLOW_DELETE: 'workflow:delete',
|
|
59
|
+
FRAME_READ: 'frame:read',
|
|
60
|
+
FRAME_WRITE: 'frame:write',
|
|
61
|
+
ADMIN_MANAGE: 'admin:manage'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const ROLE_PERMISSIONS = {
|
|
65
|
+
[ROLES.ADMIN]: Object.values(PERMISSIONS),
|
|
66
|
+
[ROLES.USER]: [
|
|
67
|
+
PERMISSIONS.CHROME_CONTROL,
|
|
68
|
+
PERMISSIONS.WORKFLOW_READ,
|
|
69
|
+
PERMISSIONS.WORKFLOW_WRITE,
|
|
70
|
+
PERMISSIONS.WORKFLOW_DELETE,
|
|
71
|
+
PERMISSIONS.FRAME_READ,
|
|
72
|
+
PERMISSIONS.FRAME_WRITE
|
|
73
|
+
],
|
|
74
|
+
[ROLES.READONLY]: [
|
|
75
|
+
PERMISSIONS.WORKFLOW_READ,
|
|
76
|
+
PERMISSIONS.FRAME_READ
|
|
77
|
+
],
|
|
78
|
+
[MCP_SERVICE_ROLES.MCP_CLIENT]: [
|
|
79
|
+
PERMISSIONS.WORKFLOW_READ,
|
|
80
|
+
PERMISSIONS.FRAME_READ
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Generate secure API key
|
|
85
|
+
export function generateApiKey(name, role = ROLES.USER) {
|
|
86
|
+
const apiKey = `cp_${uuidv4().replace(/-/g, '')}`;
|
|
87
|
+
const hashedKey = bcrypt.hashSync(apiKey, 12);
|
|
88
|
+
|
|
89
|
+
const keyData = {
|
|
90
|
+
id: uuidv4(),
|
|
91
|
+
name,
|
|
92
|
+
role,
|
|
93
|
+
hashedKey,
|
|
94
|
+
createdAt: new Date().toISOString(),
|
|
95
|
+
lastUsed: null,
|
|
96
|
+
active: true
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
apiKeys.push(keyData);
|
|
100
|
+
saveApiKeys();
|
|
101
|
+
|
|
102
|
+
return { apiKey, keyData: { ...keyData, hashedKey: undefined } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate API key
|
|
106
|
+
function validateApiKey(apiKey) {
|
|
107
|
+
if (!apiKey) return null;
|
|
108
|
+
|
|
109
|
+
for (const keyData of apiKeys) {
|
|
110
|
+
if (keyData.active && bcrypt.compareSync(apiKey, keyData.hashedKey)) {
|
|
111
|
+
// Update last used timestamp
|
|
112
|
+
keyData.lastUsed = new Date().toISOString();
|
|
113
|
+
saveApiKeys();
|
|
114
|
+
return keyData;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Generate JWT token
|
|
121
|
+
export function generateJWT(payload) {
|
|
122
|
+
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Verify JWT token
|
|
126
|
+
function verifyJWT(token) {
|
|
127
|
+
try {
|
|
128
|
+
return jwt.verify(token, JWT_SECRET);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Authentication middleware
|
|
135
|
+
export function authenticate(req, res, next) {
|
|
136
|
+
// Debug logging for authentication flow
|
|
137
|
+
console.log(`[AUTH-DEBUG] ${new Date().toISOString()} - ${req.method} ${req.originalUrl}`);
|
|
138
|
+
console.log(`[AUTH-DEBUG] Client IP: ${req.ip}`);
|
|
139
|
+
console.log(`[AUTH-DEBUG] X-Forwarded-For: ${req.headers['x-forwarded-for'] || 'none'}`);
|
|
140
|
+
console.log(`[AUTH-DEBUG] User-Agent: ${req.headers['user-agent'] || 'none'}`);
|
|
141
|
+
console.log(`[AUTH-DEBUG] Authorization header present: ${!!req.headers.authorization}`);
|
|
142
|
+
console.log(`[AUTH-DEBUG] X-API-Key header present: ${!!req.headers['x-api-key']}`);
|
|
143
|
+
console.log(`[AUTH-DEBUG] X-Service-Token header present: ${!!req.headers['x-service-token']}`);
|
|
144
|
+
|
|
145
|
+
// Allow localhost requests from Chrome extension
|
|
146
|
+
const isLocalhost = req.ip === '::1' || req.ip === '127.0.0.1' || req.ip === '::ffff:127.0.0.1';
|
|
147
|
+
console.log(`[AUTH-DEBUG] Is localhost: ${isLocalhost}`);
|
|
148
|
+
|
|
149
|
+
if (isLocalhost) {
|
|
150
|
+
// Set a synthetic user for localhost requests with explicit permissions
|
|
151
|
+
req.user = {
|
|
152
|
+
id: 'localhost',
|
|
153
|
+
name: 'Chrome Extension (Localhost)',
|
|
154
|
+
role: 'USER',
|
|
155
|
+
authMethod: 'localhost-bypass',
|
|
156
|
+
permissions: [
|
|
157
|
+
PERMISSIONS.CHROME_CONTROL,
|
|
158
|
+
PERMISSIONS.WORKFLOW_READ,
|
|
159
|
+
PERMISSIONS.WORKFLOW_WRITE,
|
|
160
|
+
PERMISSIONS.WORKFLOW_DELETE,
|
|
161
|
+
PERMISSIONS.FRAME_READ,
|
|
162
|
+
PERMISSIONS.FRAME_WRITE
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
console.log(`[AUTH-DEBUG] ✅ Localhost bypass granted for user:`, req.user);
|
|
166
|
+
return next();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const authHeader = req.headers.authorization;
|
|
170
|
+
const apiKey = req.headers['x-api-key'];
|
|
171
|
+
const serviceToken = req.headers['x-service-token'];
|
|
172
|
+
|
|
173
|
+
let user = null;
|
|
174
|
+
|
|
175
|
+
// Check Bearer token (JWT)
|
|
176
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
177
|
+
const token = authHeader.substring(7);
|
|
178
|
+
const decoded = verifyJWT(token);
|
|
179
|
+
if (decoded) {
|
|
180
|
+
user = decoded;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check API key
|
|
185
|
+
if (!user && apiKey) {
|
|
186
|
+
const keyData = validateApiKey(apiKey);
|
|
187
|
+
if (keyData) {
|
|
188
|
+
user = {
|
|
189
|
+
id: keyData.id,
|
|
190
|
+
name: keyData.name,
|
|
191
|
+
role: keyData.role,
|
|
192
|
+
authMethod: 'api-key'
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check MCP service token
|
|
198
|
+
if (!user && serviceToken) {
|
|
199
|
+
const isValidServiceToken = validateServiceToken(serviceToken);
|
|
200
|
+
if (isValidServiceToken) {
|
|
201
|
+
user = {
|
|
202
|
+
id: 'mcp-service',
|
|
203
|
+
name: 'MCP Service',
|
|
204
|
+
role: MCP_SERVICE_ROLES.MCP_CLIENT,
|
|
205
|
+
authMethod: 'service-token'
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!user) {
|
|
211
|
+
console.log(`[AUTH-DEBUG] ❌ Authentication failed - no valid credentials found`);
|
|
212
|
+
return res.status(401).json({
|
|
213
|
+
error: 'Authentication required',
|
|
214
|
+
message: 'Please provide a valid API key, Bearer token, or service token',
|
|
215
|
+
debug: {
|
|
216
|
+
clientIp: req.ip,
|
|
217
|
+
userAgent: req.headers['user-agent'],
|
|
218
|
+
timestamp: new Date().toISOString()
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log(`[AUTH-DEBUG] ✅ Authentication successful for user:`, {
|
|
224
|
+
id: user.id,
|
|
225
|
+
name: user.name,
|
|
226
|
+
role: user.role,
|
|
227
|
+
authMethod: user.authMethod
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
req.user = user;
|
|
231
|
+
next();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Authorization middleware
|
|
235
|
+
export function authorize(requiredPermission) {
|
|
236
|
+
return (req, res, next) => {
|
|
237
|
+
const user = req.user;
|
|
238
|
+
if (!user) {
|
|
239
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Get permissions from role-based mapping
|
|
243
|
+
const rolePermissions = ROLE_PERMISSIONS[user.role] || [];
|
|
244
|
+
|
|
245
|
+
// Also check explicit permissions on user object (for localhost bypass, etc.)
|
|
246
|
+
const explicitPermissions = user.permissions || [];
|
|
247
|
+
|
|
248
|
+
// Combine both permission sources
|
|
249
|
+
const userPermissions = [...new Set([...rolePermissions, ...explicitPermissions])];
|
|
250
|
+
|
|
251
|
+
if (!userPermissions.includes(requiredPermission)) {
|
|
252
|
+
return res.status(403).json({
|
|
253
|
+
error: 'Insufficient permissions',
|
|
254
|
+
required: requiredPermission,
|
|
255
|
+
userRole: user.role,
|
|
256
|
+
userPermissions: userPermissions
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
next();
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Admin-only middleware
|
|
265
|
+
export function requireAdmin(req, res, next) {
|
|
266
|
+
if (!req.user || req.user.role !== ROLES.ADMIN) {
|
|
267
|
+
return res.status(403).json({
|
|
268
|
+
error: 'Admin access required'
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
next();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// API key management functions
|
|
275
|
+
export function listApiKeys() {
|
|
276
|
+
return apiKeys.map(key => ({
|
|
277
|
+
id: key.id,
|
|
278
|
+
name: key.name,
|
|
279
|
+
role: key.role,
|
|
280
|
+
createdAt: key.createdAt,
|
|
281
|
+
lastUsed: key.lastUsed,
|
|
282
|
+
active: key.active
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function revokeApiKey(keyId) {
|
|
287
|
+
const key = apiKeys.find(k => k.id === keyId);
|
|
288
|
+
if (key) {
|
|
289
|
+
key.active = false;
|
|
290
|
+
saveApiKeys();
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function activateApiKey(keyId) {
|
|
297
|
+
const key = apiKeys.find(k => k.id === keyId);
|
|
298
|
+
if (key) {
|
|
299
|
+
key.active = true;
|
|
300
|
+
saveApiKeys();
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function deleteApiKey(keyId) {
|
|
307
|
+
const index = apiKeys.findIndex(k => k.id === keyId);
|
|
308
|
+
if (index !== -1) {
|
|
309
|
+
apiKeys.splice(index, 1);
|
|
310
|
+
saveApiKeys();
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Initialize with default admin key if no keys exist
|
|
317
|
+
if (apiKeys.length === 0) {
|
|
318
|
+
const defaultAdmin = generateApiKey('Default Admin', ROLES.ADMIN);
|
|
319
|
+
console.log('=== Chrome Debug Security Initialization ===');
|
|
320
|
+
console.log('Generated default admin API key:');
|
|
321
|
+
console.log(`API Key: ${defaultAdmin.apiKey}`);
|
|
322
|
+
console.log('Please save this key securely and create additional keys as needed.');
|
|
323
|
+
console.log('============================================');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Generate MCP service token
|
|
327
|
+
export function generateServiceToken() {
|
|
328
|
+
const serviceToken = `mcp_${uuidv4().replace(/-/g, '')}`;
|
|
329
|
+
return serviceToken;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Validate MCP service token
|
|
333
|
+
function validateServiceToken(token) {
|
|
334
|
+
const expectedToken = process.env.MCP_SERVICE_TOKEN;
|
|
335
|
+
if (!expectedToken) {
|
|
336
|
+
console.warn('MCP_SERVICE_TOKEN not set. Service authentication disabled.');
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
return token === expectedToken;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Initialize MCP service token if not set
|
|
343
|
+
function initializeMcpServiceToken() {
|
|
344
|
+
if (!process.env.MCP_SERVICE_TOKEN) {
|
|
345
|
+
const serviceToken = generateServiceToken();
|
|
346
|
+
process.env.MCP_SERVICE_TOKEN = serviceToken;
|
|
347
|
+
console.log('=== ChromeDebug MCP Service Token ===');
|
|
348
|
+
console.log(`Generated MCP service token: ${serviceToken}`);
|
|
349
|
+
console.log('This token allows MCP clients to access recordings.');
|
|
350
|
+
console.log('======================================');
|
|
351
|
+
|
|
352
|
+
// Optionally save to .env file for persistence
|
|
353
|
+
try {
|
|
354
|
+
const envPath = path.join(__dirname, '../../.env');
|
|
355
|
+
const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
|
|
356
|
+
|
|
357
|
+
if (!envContent.includes('MCP_SERVICE_TOKEN=')) {
|
|
358
|
+
const newEnvContent = envContent + (envContent.endsWith('\n') ? '' : '\n') + `MCP_SERVICE_TOKEN=${serviceToken}\n`;
|
|
359
|
+
fs.writeFileSync(envPath, newEnvContent);
|
|
360
|
+
console.log('MCP service token saved to .env file');
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.log('Could not save MCP service token to .env file:', error.message);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Initialize MCP service token on module load
|
|
369
|
+
initializeMcpServiceToken();
|
|
370
|
+
|
|
371
|
+
export { ROLES, PERMISSIONS, ROLE_PERMISSIONS, MCP_SERVICE_ROLES, validateServiceToken };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import helmet from 'helmet';
|
|
2
|
+
import rateLimit from 'express-rate-limit';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
|
|
5
|
+
// Environment configuration
|
|
6
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
7
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
8
|
+
|
|
9
|
+
// Trusted origins configuration
|
|
10
|
+
const trustedOrigins = [
|
|
11
|
+
// Chrome extension origins
|
|
12
|
+
'chrome-extension://*',
|
|
13
|
+
// VS Code extension
|
|
14
|
+
'vscode-webview://*',
|
|
15
|
+
// Local development (always allowed for Chrome extension compatibility)
|
|
16
|
+
'http://localhost:*',
|
|
17
|
+
'http://127.0.0.1:*',
|
|
18
|
+
'http://0.0.0.0:*',
|
|
19
|
+
// Add production origins from environment
|
|
20
|
+
...(process.env.TRUSTED_ORIGINS ? process.env.TRUSTED_ORIGINS.split(',') : [])
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// CORS configuration
|
|
24
|
+
export const corsOptions = {
|
|
25
|
+
origin: function (origin, callback) {
|
|
26
|
+
// Allow requests with no origin (mobile apps, Postman, etc.)
|
|
27
|
+
if (!origin) return callback(null, true);
|
|
28
|
+
|
|
29
|
+
// Check if origin matches any trusted pattern
|
|
30
|
+
const isAllowed = trustedOrigins.some(pattern => {
|
|
31
|
+
if (pattern.includes('*')) {
|
|
32
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
33
|
+
return regex.test(origin);
|
|
34
|
+
}
|
|
35
|
+
return pattern === origin;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (isAllowed) {
|
|
39
|
+
callback(null, true);
|
|
40
|
+
} else {
|
|
41
|
+
console.warn(`CORS: Blocked origin ${origin}`);
|
|
42
|
+
callback(new Error('Not allowed by CORS policy'));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
credentials: true,
|
|
46
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
47
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Requested-With'],
|
|
48
|
+
exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset']
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Security headers configuration
|
|
52
|
+
export const securityHeaders = helmet({
|
|
53
|
+
contentSecurityPolicy: {
|
|
54
|
+
directives: {
|
|
55
|
+
defaultSrc: ["'self'"],
|
|
56
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
57
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
58
|
+
imgSrc: ["'self'", "data:", "blob:"],
|
|
59
|
+
connectSrc: ["'self'", "ws:", "wss:"],
|
|
60
|
+
fontSrc: ["'self'"],
|
|
61
|
+
objectSrc: ["'none'"],
|
|
62
|
+
mediaSrc: ["'self'", "blob:"],
|
|
63
|
+
frameSrc: ["'none'"]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
crossOriginEmbedderPolicy: false, // Allow Chrome extension integration
|
|
67
|
+
crossOriginOpenerPolicy: false,
|
|
68
|
+
crossOriginResourcePolicy: { policy: "cross-origin" },
|
|
69
|
+
hsts: {
|
|
70
|
+
maxAge: 31536000,
|
|
71
|
+
includeSubDomains: true,
|
|
72
|
+
preload: true
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Rate limiting configuration
|
|
77
|
+
export const createRateLimit = (options = {}) => {
|
|
78
|
+
return rateLimit({
|
|
79
|
+
windowMs: options.windowMs || 15 * 60 * 1000, // 15 minutes
|
|
80
|
+
max: options.max || 100, // Limit each IP to 100 requests per windowMs
|
|
81
|
+
message: {
|
|
82
|
+
error: 'Too many requests',
|
|
83
|
+
retryAfter: options.windowMs || 15 * 60 * 1000
|
|
84
|
+
},
|
|
85
|
+
standardHeaders: true,
|
|
86
|
+
legacyHeaders: false,
|
|
87
|
+
// Use user ID for authenticated requests, default for unauthenticated
|
|
88
|
+
keyGenerator: (req) => {
|
|
89
|
+
if (req.user?.id) {
|
|
90
|
+
return `user:${req.user.id}`;
|
|
91
|
+
}
|
|
92
|
+
// Let express-rate-limit handle IP address properly (including IPv6)
|
|
93
|
+
return undefined;
|
|
94
|
+
},
|
|
95
|
+
skip: (req) => {
|
|
96
|
+
// Skip rate limiting for certain endpoints in development
|
|
97
|
+
if (isDevelopment && req.path === '/chromedebug/status') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Different rate limits for different endpoint types
|
|
106
|
+
export const rateLimits = {
|
|
107
|
+
// General API rate limit
|
|
108
|
+
general: createRateLimit({
|
|
109
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
110
|
+
max: 1000 // 1000 requests per 15 minutes
|
|
111
|
+
}),
|
|
112
|
+
|
|
113
|
+
// Authentication endpoints
|
|
114
|
+
auth: createRateLimit({
|
|
115
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
116
|
+
max: 10 // 10 auth attempts per 15 minutes
|
|
117
|
+
}),
|
|
118
|
+
|
|
119
|
+
// File upload endpoints
|
|
120
|
+
upload: createRateLimit({
|
|
121
|
+
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
122
|
+
max: 20 // 20 uploads per 5 minutes
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
// Chrome control endpoints (more restrictive)
|
|
126
|
+
chromeControl: createRateLimit({
|
|
127
|
+
windowMs: 1 * 60 * 1000, // 1 minute
|
|
128
|
+
max: 60 // 60 control actions per minute
|
|
129
|
+
}),
|
|
130
|
+
|
|
131
|
+
// Status endpoints (less restrictive)
|
|
132
|
+
status: createRateLimit({
|
|
133
|
+
windowMs: 1 * 60 * 1000, // 1 minute
|
|
134
|
+
max: 100 // 100 status checks per minute
|
|
135
|
+
})
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Request size limits
|
|
139
|
+
export const requestSizeLimits = {
|
|
140
|
+
// Standard JSON requests
|
|
141
|
+
json: '10mb',
|
|
142
|
+
|
|
143
|
+
// Form data (for file uploads)
|
|
144
|
+
urlencoded: '10mb',
|
|
145
|
+
|
|
146
|
+
// Large uploads (frame batches, screenshots)
|
|
147
|
+
largeUpload: '50mb'
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Security audit logging
|
|
151
|
+
export function securityLogger(req, res, next) {
|
|
152
|
+
const start = Date.now();
|
|
153
|
+
|
|
154
|
+
// Log sensitive operations
|
|
155
|
+
const sensitiveEndpoints = [
|
|
156
|
+
'/chromedebug/workflow-recording',
|
|
157
|
+
'/chromedebug/frame-batch',
|
|
158
|
+
'/chromedebug/evaluate',
|
|
159
|
+
'/chromedebug/launch',
|
|
160
|
+
'/chromedebug/navigate'
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const isSensitive = sensitiveEndpoints.some(endpoint =>
|
|
164
|
+
req.path.startsWith(endpoint)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (isSensitive) {
|
|
168
|
+
console.log(`[Security] ${req.method} ${req.path} - User: ${req.user?.name || 'unauthenticated'} - IP: ${req.ip}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Track response time for monitoring
|
|
172
|
+
res.on('finish', () => {
|
|
173
|
+
const duration = Date.now() - start;
|
|
174
|
+
if (duration > 5000) { // Log slow requests
|
|
175
|
+
console.warn(`[Performance] Slow request: ${req.method} ${req.path} - ${duration}ms`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
next();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Request validation middleware
|
|
183
|
+
export function validateRequest(schema) {
|
|
184
|
+
return (req, res, next) => {
|
|
185
|
+
const { error, value } = schema.validate(req.body, {
|
|
186
|
+
abortEarly: false,
|
|
187
|
+
stripUnknown: true
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (error) {
|
|
191
|
+
return res.status(400).json({
|
|
192
|
+
error: 'Validation failed',
|
|
193
|
+
details: error.details.map(detail => ({
|
|
194
|
+
field: detail.path.join('.'),
|
|
195
|
+
message: detail.message
|
|
196
|
+
}))
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
req.validatedBody = value;
|
|
201
|
+
next();
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Error handling for security middleware
|
|
206
|
+
export function securityErrorHandler(err, req, res, next) {
|
|
207
|
+
// CORS errors
|
|
208
|
+
if (err.message.includes('CORS')) {
|
|
209
|
+
return res.status(403).json({
|
|
210
|
+
error: 'CORS policy violation',
|
|
211
|
+
message: 'Origin not allowed'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Rate limiting errors
|
|
216
|
+
if (err.status === 429) {
|
|
217
|
+
return res.status(429).json({
|
|
218
|
+
error: 'Rate limit exceeded',
|
|
219
|
+
message: 'Too many requests, please try again later'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Security headers errors
|
|
224
|
+
if (err.code === 'EBADCSRFTOKEN') {
|
|
225
|
+
return res.status(403).json({
|
|
226
|
+
error: 'Invalid CSRF token'
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Default security error response
|
|
231
|
+
console.error('[Security Error]', err);
|
|
232
|
+
res.status(500).json({
|
|
233
|
+
error: 'Security error',
|
|
234
|
+
message: 'An internal security error occurred'
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// IP whitelisting (if needed)
|
|
239
|
+
export function createIPWhitelist(allowedIPs = []) {
|
|
240
|
+
return (req, res, next) => {
|
|
241
|
+
if (allowedIPs.length === 0) {
|
|
242
|
+
return next(); // No restrictions if no IPs specified
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const clientIP = req.ip || req.connection.remoteAddress;
|
|
246
|
+
|
|
247
|
+
if (!allowedIPs.includes(clientIP)) {
|
|
248
|
+
console.warn(`[Security] Blocked IP: ${clientIP}`);
|
|
249
|
+
return res.status(403).json({
|
|
250
|
+
error: 'IP not whitelisted'
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
next();
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default {
|
|
259
|
+
corsOptions,
|
|
260
|
+
securityHeaders,
|
|
261
|
+
rateLimits,
|
|
262
|
+
requestSizeLimits,
|
|
263
|
+
securityLogger,
|
|
264
|
+
validateRequest,
|
|
265
|
+
securityErrorHandler,
|
|
266
|
+
createIPWhitelist
|
|
267
|
+
};
|