@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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Transformation Utility
|
|
3
|
+
* Transforms Chrome extension logs to match associateLogsSchema compliance
|
|
4
|
+
*
|
|
5
|
+
* CONTRACT A: associateLogsSchema requires only {level, message, timestamp, args}
|
|
6
|
+
* CONTRACT B: Remove non-schema fields while preserving all valid data
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Transform logs to match associateLogsSchema format
|
|
11
|
+
* Filters out non-schema fields like 'source', 'type' while preserving valid fields
|
|
12
|
+
*
|
|
13
|
+
* @param {Array} logs - Array of log objects from Chrome extension
|
|
14
|
+
* @returns {Array} - Transformed logs matching associateLogsSchema
|
|
15
|
+
*/
|
|
16
|
+
export function transformLogsForSchema(logs) {
|
|
17
|
+
if (!Array.isArray(logs)) {
|
|
18
|
+
return logs;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return logs.map(log => {
|
|
22
|
+
// Handle null/undefined/non-object logs gracefully
|
|
23
|
+
if (!log || typeof log !== 'object') {
|
|
24
|
+
return log; // Return as-is, let schema validation handle it
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Only include fields that are allowed by associateLogsSchema
|
|
28
|
+
const transformedLog = {};
|
|
29
|
+
|
|
30
|
+
// Required fields
|
|
31
|
+
if (log.level !== undefined) {
|
|
32
|
+
transformedLog.level = log.level;
|
|
33
|
+
}
|
|
34
|
+
if (log.message !== undefined) {
|
|
35
|
+
transformedLog.message = log.message;
|
|
36
|
+
}
|
|
37
|
+
if (log.timestamp !== undefined) {
|
|
38
|
+
transformedLog.timestamp = log.timestamp;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Optional fields
|
|
42
|
+
if (log.args !== undefined) {
|
|
43
|
+
transformedLog.args = log.args;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Explicitly exclude non-schema fields:
|
|
47
|
+
// - source (from Chrome extension test harness)
|
|
48
|
+
// - type (from Chrome extension)
|
|
49
|
+
// - any other fields not in schema
|
|
50
|
+
|
|
51
|
+
return transformedLog;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Transform full associate-logs request payload
|
|
57
|
+
* Preserves sessionId and transforms logs array
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} requestPayload - Request with sessionId and logs
|
|
60
|
+
* @returns {Object} - Transformed payload ready for schema validation
|
|
61
|
+
*/
|
|
62
|
+
export function transformAssociateLogsRequest(requestPayload) {
|
|
63
|
+
if (!requestPayload || typeof requestPayload !== 'object') {
|
|
64
|
+
return requestPayload;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const transformed = { ...requestPayload };
|
|
68
|
+
|
|
69
|
+
if (transformed.logs) {
|
|
70
|
+
transformed.logs = transformLogsForSchema(transformed.logs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return transformed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validation helper - checks if logs contain non-schema fields
|
|
78
|
+
* Used for logging/diagnostics
|
|
79
|
+
*
|
|
80
|
+
* @param {Array} logs - Array of log objects
|
|
81
|
+
* @returns {Object} - Analysis of field compliance
|
|
82
|
+
*/
|
|
83
|
+
export function analyzeLogFieldCompliance(logs) {
|
|
84
|
+
if (!Array.isArray(logs)) {
|
|
85
|
+
return { isCompliant: false, reason: 'logs is not an array' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const allowedFields = new Set(['level', 'message', 'timestamp', 'args']);
|
|
89
|
+
const nonSchemaFields = new Set();
|
|
90
|
+
const missingRequiredFields = new Set();
|
|
91
|
+
|
|
92
|
+
for (const log of logs) {
|
|
93
|
+
if (!log || typeof log !== 'object') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for non-schema fields
|
|
98
|
+
for (const field of Object.keys(log)) {
|
|
99
|
+
if (!allowedFields.has(field)) {
|
|
100
|
+
nonSchemaFields.add(field);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check for missing required fields
|
|
105
|
+
const requiredFields = ['level', 'message', 'timestamp'];
|
|
106
|
+
for (const required of requiredFields) {
|
|
107
|
+
if (!(required in log)) {
|
|
108
|
+
missingRequiredFields.add(required);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
isCompliant: nonSchemaFields.size === 0 && missingRequiredFields.size === 0,
|
|
115
|
+
nonSchemaFields: Array.from(nonSchemaFields),
|
|
116
|
+
missingRequiredFields: Array.from(missingRequiredFields),
|
|
117
|
+
totalLogs: logs.length
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default {
|
|
122
|
+
transformLogsForSchema,
|
|
123
|
+
transformAssociateLogsRequest,
|
|
124
|
+
analyzeLogFieldCompliance
|
|
125
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import Joi from 'joi';
|
|
2
|
+
|
|
3
|
+
// Common validation patterns
|
|
4
|
+
const patterns = {
|
|
5
|
+
sessionId: Joi.string().pattern(/^[a-zA-Z0-9_-]+$/).min(1).max(100),
|
|
6
|
+
url: Joi.string().uri({ scheme: ['http', 'https'] }).max(2048),
|
|
7
|
+
selector: Joi.string().min(1).max(1000),
|
|
8
|
+
expression: Joi.string().min(1).max(10000),
|
|
9
|
+
recordingId: Joi.string().pattern(/^[a-zA-Z0-9_-]+$/).min(1).max(100),
|
|
10
|
+
frameIndex: Joi.number().integer().min(0).max(999999)
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Workflow recording schemas
|
|
14
|
+
export const workflowRecordingSchema = Joi.object({
|
|
15
|
+
sessionId: patterns.sessionId.required(),
|
|
16
|
+
url: patterns.url.optional(),
|
|
17
|
+
title: Joi.string().max(500).optional(),
|
|
18
|
+
includeLogs: Joi.boolean().default(false),
|
|
19
|
+
actions: Joi.array().items(
|
|
20
|
+
Joi.object({
|
|
21
|
+
type: Joi.string().required(),
|
|
22
|
+
timestamp: Joi.number().required(),
|
|
23
|
+
data: Joi.object().optional()
|
|
24
|
+
})
|
|
25
|
+
).required(),
|
|
26
|
+
logs: Joi.array().items(
|
|
27
|
+
Joi.object({
|
|
28
|
+
level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug').required(),
|
|
29
|
+
message: Joi.string().required(),
|
|
30
|
+
timestamp: Joi.number().required(),
|
|
31
|
+
args: Joi.array().optional()
|
|
32
|
+
})
|
|
33
|
+
).optional(),
|
|
34
|
+
functionTraces: Joi.array().items(
|
|
35
|
+
Joi.object({
|
|
36
|
+
type: Joi.string().optional(),
|
|
37
|
+
component: Joi.string().optional(),
|
|
38
|
+
args: Joi.array().optional(),
|
|
39
|
+
timestamp: Joi.number().optional(),
|
|
40
|
+
stack: Joi.string().optional()
|
|
41
|
+
})
|
|
42
|
+
).optional(),
|
|
43
|
+
name: Joi.any().optional(),
|
|
44
|
+
screenshotSettings: Joi.object({
|
|
45
|
+
width: Joi.number().integer().min(100).max(4000).optional(),
|
|
46
|
+
height: Joi.number().integer().min(100).max(4000).optional(),
|
|
47
|
+
quality: Joi.number().min(10).max(100).optional()
|
|
48
|
+
}).optional()
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Frame batch schema
|
|
52
|
+
export const frameBatchSchema = Joi.object({
|
|
53
|
+
sessionId: patterns.sessionId.required(),
|
|
54
|
+
sessionName: Joi.string().max(200).optional().allow(null), // Optional session name for recordings
|
|
55
|
+
frames: Joi.array().items(
|
|
56
|
+
Joi.object({
|
|
57
|
+
timestamp: Joi.number().required(),
|
|
58
|
+
absoluteTimestamp: Joi.number().optional(), // Chrome extension sends this
|
|
59
|
+
imageData: Joi.string().required(), // Base64 encoded image
|
|
60
|
+
consoleLog: Joi.string().optional(),
|
|
61
|
+
frameIndex: Joi.number().integer().min(0).optional(),
|
|
62
|
+
index: Joi.number().integer().min(0).optional(), // Chrome extension sends this
|
|
63
|
+
logs: Joi.array().items(
|
|
64
|
+
Joi.object({
|
|
65
|
+
level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug').required(),
|
|
66
|
+
message: Joi.string().required(),
|
|
67
|
+
timestamp: Joi.number().required(),
|
|
68
|
+
args: Joi.array().optional()
|
|
69
|
+
})
|
|
70
|
+
).optional(), // Chrome extension sends this
|
|
71
|
+
interactions: Joi.array().items(
|
|
72
|
+
Joi.object({
|
|
73
|
+
type: Joi.string().valid('click', 'scroll', 'keydown', 'drag').required(),
|
|
74
|
+
timestamp: Joi.number().required(),
|
|
75
|
+
x: Joi.number().when('type', { is: 'click', then: Joi.required(), otherwise: Joi.optional() }),
|
|
76
|
+
y: Joi.number().when('type', { is: 'click', then: Joi.required(), otherwise: Joi.optional() }),
|
|
77
|
+
target: Joi.string().optional(),
|
|
78
|
+
targetId: Joi.string().optional(),
|
|
79
|
+
scrollX: Joi.number().when('type', { is: 'scroll', then: Joi.required(), otherwise: Joi.optional() }),
|
|
80
|
+
scrollY: Joi.number().when('type', { is: 'scroll', then: Joi.required(), otherwise: Joi.optional() }),
|
|
81
|
+
key: Joi.string().when('type', { is: 'keydown', then: Joi.required(), otherwise: Joi.optional() })
|
|
82
|
+
})
|
|
83
|
+
).optional().default([]) // Interaction tracking data
|
|
84
|
+
})
|
|
85
|
+
).min(1).max(1000).required()
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Associate logs schema
|
|
89
|
+
export const associateLogsSchema = Joi.object({
|
|
90
|
+
sessionId: patterns.sessionId.required(),
|
|
91
|
+
logs: Joi.array().items(
|
|
92
|
+
Joi.object({
|
|
93
|
+
level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug').required(),
|
|
94
|
+
message: Joi.string().required(),
|
|
95
|
+
timestamp: Joi.number().required(),
|
|
96
|
+
args: Joi.array().optional()
|
|
97
|
+
})
|
|
98
|
+
).required()
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Stream logs schema for batched real-time log streaming
|
|
102
|
+
export const streamLogsSchema = Joi.object({
|
|
103
|
+
sessionId: patterns.sessionId.required(),
|
|
104
|
+
logs: Joi.array().items(
|
|
105
|
+
Joi.object({
|
|
106
|
+
level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug', 'trace', 'table', 'dir', 'group', 'groupEnd', 'time', 'timeEnd', 'count').required(),
|
|
107
|
+
message: Joi.string().required(),
|
|
108
|
+
timestamp: Joi.number().required(),
|
|
109
|
+
sequence: Joi.number().integer().min(0).required(),
|
|
110
|
+
args: Joi.array().optional()
|
|
111
|
+
})
|
|
112
|
+
).min(1).max(100).required() // Limit batch size
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// DOM intent schema
|
|
116
|
+
export const domIntentSchema = Joi.object({
|
|
117
|
+
selector: patterns.selector.required(),
|
|
118
|
+
instruction: Joi.string().max(1000).optional().allow(''),
|
|
119
|
+
elementInfo: Joi.object({
|
|
120
|
+
tagName: Joi.string().optional(),
|
|
121
|
+
className: Joi.string().optional(),
|
|
122
|
+
id: Joi.string().optional(),
|
|
123
|
+
textContent: Joi.string().max(1000).optional(),
|
|
124
|
+
attributes: Joi.object().optional()
|
|
125
|
+
}).optional()
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Chrome control schemas
|
|
129
|
+
export const navigateSchema = Joi.object({
|
|
130
|
+
url: patterns.url.required()
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const evaluateSchema = Joi.object({
|
|
134
|
+
expression: patterns.expression.required()
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Screen interactions schema
|
|
138
|
+
export const screenInteractionsSchema = Joi.object({
|
|
139
|
+
interactions: Joi.array().items(
|
|
140
|
+
Joi.object({
|
|
141
|
+
type: Joi.string().valid('click', 'scroll', 'keypress', 'mousemove', 'input', 'change', 'drag').required(),
|
|
142
|
+
timestamp: Joi.number().required(),
|
|
143
|
+
x: Joi.number().optional(),
|
|
144
|
+
y: Joi.number().optional(),
|
|
145
|
+
key: Joi.string().optional(),
|
|
146
|
+
target: Joi.string().optional(),
|
|
147
|
+
// Basic element fields that were previously blocked
|
|
148
|
+
selector: Joi.string().optional(),
|
|
149
|
+
xpath: Joi.string().optional(),
|
|
150
|
+
text: Joi.string().allow('').optional(),
|
|
151
|
+
value: Joi.string().allow('').optional(),
|
|
152
|
+
// Enhanced capture fields
|
|
153
|
+
element_html: Joi.string().optional(),
|
|
154
|
+
component_data: Joi.alternatives().try(Joi.string(), Joi.object()).optional(),
|
|
155
|
+
event_handlers: Joi.alternatives().try(Joi.string(), Joi.object()).optional(),
|
|
156
|
+
element_state: Joi.alternatives().try(Joi.string(), Joi.object()).optional(),
|
|
157
|
+
performance_metrics: Joi.alternatives().try(Joi.string(), Joi.object()).optional(),
|
|
158
|
+
frameIndex: Joi.number().integer().min(0).optional()
|
|
159
|
+
})
|
|
160
|
+
).optional().default([]) // Allow empty arrays - Chrome extension sends empty arrays when no interactions occur during frame capture
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// API key management schemas
|
|
164
|
+
export const createApiKeySchema = Joi.object({
|
|
165
|
+
name: Joi.string().min(1).max(100).required(),
|
|
166
|
+
role: Joi.string().valid('admin', 'user', 'readonly').default('user')
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const updateApiKeySchema = Joi.object({
|
|
170
|
+
name: Joi.string().min(1).max(100).optional(),
|
|
171
|
+
active: Joi.boolean().optional()
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Parameter validation schemas
|
|
175
|
+
export const sessionIdParam = Joi.object({
|
|
176
|
+
sessionId: patterns.sessionId.required()
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const recordingIdParam = Joi.object({
|
|
180
|
+
recordingId: patterns.recordingId.required()
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export const frameParams = Joi.object({
|
|
184
|
+
sessionId: patterns.sessionId.required(),
|
|
185
|
+
frameIndex: patterns.frameIndex.required()
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Query parameter schemas
|
|
189
|
+
export const pageContentQuery = Joi.object({
|
|
190
|
+
includeText: Joi.boolean().default(true),
|
|
191
|
+
includeHtml: Joi.boolean().default(false),
|
|
192
|
+
includeStructure: Joi.boolean().default(true)
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const paginationQuery = Joi.object({
|
|
196
|
+
page: Joi.number().integer().min(1).default(1),
|
|
197
|
+
limit: Joi.number().integer().min(1).max(1000).default(50),
|
|
198
|
+
search: Joi.string().max(500).optional()
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// WebSocket message schemas
|
|
202
|
+
export const wsMessageSchema = Joi.object({
|
|
203
|
+
type: Joi.string().valid('element_selected', 'get_status').required(),
|
|
204
|
+
data: Joi.object().optional()
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// File upload validation
|
|
208
|
+
export const validateFileUpload = (file, allowedTypes = ['image/jpeg', 'image/png', 'image/webp']) => {
|
|
209
|
+
const errors = [];
|
|
210
|
+
|
|
211
|
+
if (!file) {
|
|
212
|
+
errors.push('File is required');
|
|
213
|
+
return errors;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check file size (50MB limit)
|
|
217
|
+
const maxSize = 50 * 1024 * 1024;
|
|
218
|
+
if (file.size > maxSize) {
|
|
219
|
+
errors.push(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check file type
|
|
223
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
224
|
+
errors.push(`File type ${file.mimetype} not allowed. Allowed types: ${allowedTypes.join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return errors;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Sanitization helpers
|
|
231
|
+
export const sanitizers = {
|
|
232
|
+
// Remove potentially dangerous HTML/JS
|
|
233
|
+
sanitizeHtml: (str) => {
|
|
234
|
+
if (typeof str !== 'string') return str;
|
|
235
|
+
return str
|
|
236
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
237
|
+
.replace(/javascript:/gi, '')
|
|
238
|
+
.replace(/on\w+\s*=/gi, '');
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Sanitize SQL-like patterns
|
|
242
|
+
sanitizeSql: (str) => {
|
|
243
|
+
if (typeof str !== 'string') return str;
|
|
244
|
+
return str.replace(/['"`;\\]/g, '');
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// Sanitize file paths
|
|
248
|
+
sanitizePath: (str) => {
|
|
249
|
+
if (typeof str !== 'string') return str;
|
|
250
|
+
return str
|
|
251
|
+
.replace(/[\.]{2,}/g, '') // Remove directory traversal
|
|
252
|
+
.replace(/\/+/g, '/') // Normalize multiple slashes
|
|
253
|
+
.replace(/^\/+/, '') // Remove leading slashes
|
|
254
|
+
.replace(/[^a-zA-Z0-9_\-\.\/]/g, ''); // Remove dangerous characters
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Validation middleware factory
|
|
259
|
+
export function createValidator(schema, target = 'body') {
|
|
260
|
+
return (req, res, next) => {
|
|
261
|
+
const data = target === 'params' ? req.params :
|
|
262
|
+
target === 'query' ? req.query : req.body;
|
|
263
|
+
|
|
264
|
+
const { error, value } = schema.validate(data, {
|
|
265
|
+
abortEarly: false,
|
|
266
|
+
stripUnknown: true,
|
|
267
|
+
allowUnknown: false
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (error) {
|
|
271
|
+
// Diagnostic logging for validation failures
|
|
272
|
+
console.log(`[Validation] Failed for ${target}:`, JSON.stringify(data, null, 2));
|
|
273
|
+
console.log(`[Validation] Errors:`, error.details.map(d => ({ field: d.path.join('.'), message: d.message, value: d.context?.value })));
|
|
274
|
+
|
|
275
|
+
return res.status(400).json({
|
|
276
|
+
error: 'Validation failed',
|
|
277
|
+
details: error.details.map(detail => ({
|
|
278
|
+
field: detail.path.join('.'),
|
|
279
|
+
message: detail.message,
|
|
280
|
+
value: detail.context?.value
|
|
281
|
+
}))
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Replace original data with validated/sanitized data
|
|
286
|
+
if (target === 'params') {
|
|
287
|
+
req.params = value;
|
|
288
|
+
} else if (target === 'query') {
|
|
289
|
+
req.query = value;
|
|
290
|
+
} else {
|
|
291
|
+
req.body = value;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
next();
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Instrumentation validation schemas
|
|
299
|
+
export const instrumentationProjectSchema = Joi.object({
|
|
300
|
+
projectPath: Joi.string().min(1).max(1000).required()
|
|
301
|
+
.custom((value, helpers) => {
|
|
302
|
+
// Prevent directory traversal attacks
|
|
303
|
+
if (value.includes('..') || value.includes('~')) {
|
|
304
|
+
return helpers.error('path.unsafe');
|
|
305
|
+
}
|
|
306
|
+
// Must be absolute path
|
|
307
|
+
if (!value.startsWith('/')) {
|
|
308
|
+
return helpers.error('path.relative');
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
}),
|
|
312
|
+
patterns: Joi.array().items(Joi.string().min(1).max(200)).default([]),
|
|
313
|
+
exclusions: Joi.array().items(Joi.string().min(1).max(200)).default([])
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
export const instrumentationOptionsSchema = Joi.object({
|
|
317
|
+
preserveComments: Joi.boolean().default(true),
|
|
318
|
+
createBackup: Joi.boolean().default(true),
|
|
319
|
+
dryRun: Joi.boolean().default(false),
|
|
320
|
+
useWorkerThreads: Joi.boolean().default(false),
|
|
321
|
+
maxPreviewFiles: Joi.number().integer().min(1).max(100).default(10),
|
|
322
|
+
instrumentationType: Joi.string().valid('auto-detect', 'react', 'vue', 'angular', 'vanilla-js').default('auto-detect'),
|
|
323
|
+
excludeTests: Joi.boolean().default(true),
|
|
324
|
+
backupStrategy: Joi.string().valid('git-stash', 'file-copy', 'none').default('git-stash')
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Git repository validation
|
|
328
|
+
export const gitRepositorySchema = Joi.object({
|
|
329
|
+
projectPath: Joi.string().required(),
|
|
330
|
+
allowUncommittedChanges: Joi.boolean().default(false),
|
|
331
|
+
requireGitRepo: Joi.boolean().default(false),
|
|
332
|
+
protectedBranches: Joi.array().items(Joi.string()).default(['main', 'master', 'production'])
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// File pattern validation with security constraints
|
|
336
|
+
export const filePatternSchema = Joi.object({
|
|
337
|
+
patterns: Joi.array().items(
|
|
338
|
+
Joi.string().pattern(/^[a-zA-Z0-9_\-\.\*\/]+$/).max(200)
|
|
339
|
+
.custom((value, helpers) => {
|
|
340
|
+
// Prevent malicious glob patterns
|
|
341
|
+
if (value.includes('/../') || value.startsWith('../')) {
|
|
342
|
+
return helpers.error('pattern.traversal');
|
|
343
|
+
}
|
|
344
|
+
// Prevent excessive wildcards that could cause DoS
|
|
345
|
+
const wildcardCount = (value.match(/\*/g) || []).length;
|
|
346
|
+
if (wildcardCount > 3) {
|
|
347
|
+
return helpers.error('pattern.excessive');
|
|
348
|
+
}
|
|
349
|
+
return value;
|
|
350
|
+
})
|
|
351
|
+
).required(),
|
|
352
|
+
exclusions: Joi.array().items(
|
|
353
|
+
Joi.string().pattern(/^[a-zA-Z0-9_\-\.\*\/]+$/).max(200)
|
|
354
|
+
).default([])
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Project boundary validation
|
|
358
|
+
export const projectBoundarySchema = Joi.object({
|
|
359
|
+
projectRoot: Joi.string().required(),
|
|
360
|
+
allowedPaths: Joi.array().items(Joi.string()).optional(),
|
|
361
|
+
restrictedPaths: Joi.array().items(Joi.string()).default([
|
|
362
|
+
'/etc', '/usr', '/var', '/home', '/root', '/System', '/Library'
|
|
363
|
+
])
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
export default {
|
|
367
|
+
workflowRecordingSchema,
|
|
368
|
+
frameBatchSchema,
|
|
369
|
+
associateLogsSchema,
|
|
370
|
+
streamLogsSchema,
|
|
371
|
+
domIntentSchema,
|
|
372
|
+
navigateSchema,
|
|
373
|
+
evaluateSchema,
|
|
374
|
+
screenInteractionsSchema,
|
|
375
|
+
createApiKeySchema,
|
|
376
|
+
updateApiKeySchema,
|
|
377
|
+
sessionIdParam,
|
|
378
|
+
recordingIdParam,
|
|
379
|
+
frameParams,
|
|
380
|
+
pageContentQuery,
|
|
381
|
+
paginationQuery,
|
|
382
|
+
wsMessageSchema,
|
|
383
|
+
instrumentationProjectSchema,
|
|
384
|
+
instrumentationOptionsSchema,
|
|
385
|
+
gitRepositorySchema,
|
|
386
|
+
filePatternSchema,
|
|
387
|
+
projectBoundarySchema,
|
|
388
|
+
validateFileUpload,
|
|
389
|
+
sanitizers,
|
|
390
|
+
createValidator
|
|
391
|
+
};
|