parse-dashboard-analytics 7.4.2 → 7.4.5

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.
Files changed (43) hide show
  1. package/Parse-Dashboard/Authentication.js +9 -4
  2. package/Parse-Dashboard/CLI/mfa.js +11 -5
  3. package/Parse-Dashboard/app.js +93 -26
  4. package/Parse-Dashboard/browser-control/BrowserControlAPI.js +468 -0
  5. package/Parse-Dashboard/browser-control/BrowserEventStream.js +294 -0
  6. package/Parse-Dashboard/browser-control/BrowserSessionManager.js +304 -0
  7. package/Parse-Dashboard/browser-control/README.md +852 -0
  8. package/Parse-Dashboard/browser-control/ServerOrchestrator.js +334 -0
  9. package/Parse-Dashboard/browser-control/index.js +27 -0
  10. package/Parse-Dashboard/browser-control/setup.js +405 -0
  11. package/Parse-Dashboard/public/bundles/120.bundle.js +1 -1
  12. package/Parse-Dashboard/public/bundles/183.bundle.js +1 -1
  13. package/Parse-Dashboard/public/bundles/19.bundle.js +1 -1
  14. package/Parse-Dashboard/public/bundles/221.bundle.js +1 -1
  15. package/Parse-Dashboard/public/bundles/372.bundle.js +1 -1
  16. package/Parse-Dashboard/public/bundles/448.bundle.js +1 -1
  17. package/Parse-Dashboard/public/bundles/573.bundle.js +1 -1
  18. package/Parse-Dashboard/public/bundles/578.bundle.js +1 -1
  19. package/Parse-Dashboard/public/bundles/584.bundle.js +1 -1
  20. package/Parse-Dashboard/public/bundles/629.bundle.js +1 -1
  21. package/Parse-Dashboard/public/bundles/643.bundle.js +1 -1
  22. package/Parse-Dashboard/public/bundles/647.bundle.js +1 -1
  23. package/Parse-Dashboard/public/bundles/701.bundle.js +1 -1
  24. package/Parse-Dashboard/public/bundles/729.bundle.js +1 -1
  25. package/Parse-Dashboard/public/bundles/75.bundle.js +1 -1
  26. package/Parse-Dashboard/public/bundles/753.bundle.js +1 -1
  27. package/Parse-Dashboard/public/bundles/817.bundle.js +1 -1
  28. package/Parse-Dashboard/public/bundles/844.bundle.js +1 -1
  29. package/Parse-Dashboard/public/bundles/862.bundle.js +1 -1
  30. package/Parse-Dashboard/public/bundles/866.bundle.js +1 -1
  31. package/Parse-Dashboard/public/bundles/874.bundle.js +1 -1
  32. package/Parse-Dashboard/public/bundles/881.bundle.js +1 -1
  33. package/Parse-Dashboard/public/bundles/929.bundle.js +1 -1
  34. package/Parse-Dashboard/public/bundles/950.bundle.js +1 -1
  35. package/Parse-Dashboard/public/bundles/97.bundle.js +1 -1
  36. package/Parse-Dashboard/public/bundles/976.bundle.js +1 -1
  37. package/Parse-Dashboard/public/bundles/dashboard.bundle.js +1 -1
  38. package/Parse-Dashboard/public/bundles/dashboard.bundle.js.LICENSE.txt +27 -33
  39. package/Parse-Dashboard/public/bundles/login.bundle.js +1 -1
  40. package/Parse-Dashboard/public/bundles/sprites.svg +294 -154
  41. package/Parse-Dashboard/server.js +33 -2
  42. package/README.md +475 -117
  43. package/package.json +68 -53
@@ -0,0 +1,294 @@
1
+ const WebSocket = require('ws');
2
+
3
+ /**
4
+ * BrowserEventStream
5
+ *
6
+ * WebSocket server for streaming real-time browser events to AI agents.
7
+ * Broadcasts console logs, network activity, navigation events, and errors.
8
+ *
9
+ * Features:
10
+ * - Real-time event streaming via WebSocket
11
+ * - Per-session event filtering
12
+ * - Automatic cleanup on disconnect
13
+ * - Support for multiple clients per session
14
+ */
15
+ class BrowserEventStream {
16
+ /**
17
+ * @param {http.Server} httpServer - HTTP server to attach WebSocket to
18
+ * @param {BrowserSessionManager} sessionManager - Session manager instance
19
+ */
20
+ constructor(httpServer, sessionManager) {
21
+ this.sessionManager = sessionManager;
22
+ this.clients = new Map(); // sessionId -> Set of WebSocket clients
23
+
24
+ // Create WebSocket server without path (we'll handle routing manually)
25
+ this.wss = new WebSocket.Server({
26
+ noServer: true,
27
+ clientTracking: true
28
+ });
29
+
30
+ // Handle WebSocket upgrade requests manually to support dynamic paths
31
+ // Path pattern: /browser-control/stream/:sessionId
32
+ httpServer.on('upgrade', (request, socket, head) => {
33
+ const pathname = new URL(request.url, 'http://localhost').pathname;
34
+
35
+ // Check if this is a browser-control stream request
36
+ if (pathname.startsWith('/browser-control/stream/')) {
37
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
38
+ this.wss.emit('connection', ws, request);
39
+ });
40
+ }
41
+ // Let other upgrade requests pass through (don't destroy socket)
42
+ });
43
+
44
+ // Handle new WebSocket connections
45
+ this.wss.on('connection', (ws, req) => {
46
+ this.handleConnection(ws, req);
47
+ });
48
+
49
+ // Listen to session manager events
50
+ this.attachSessionListeners();
51
+
52
+ console.log('Browser Event Stream WebSocket server initialized');
53
+ }
54
+
55
+ /**
56
+ * Handle new WebSocket connection
57
+ * @private
58
+ */
59
+ handleConnection(ws, req) {
60
+ // Extract session ID from URL path
61
+ // Expected format: /browser-control/stream/sess_abc123
62
+ const sessionId = this.extractSessionId(req.url);
63
+
64
+ if (!sessionId) {
65
+ ws.close(1008, 'Session ID required in URL path: /browser-control/stream/:sessionId');
66
+ return;
67
+ }
68
+
69
+ // Validate session exists
70
+ if (!this.sessionManager.hasSession(sessionId)) {
71
+ ws.close(1008, `Invalid session ID: ${sessionId}`);
72
+ return;
73
+ }
74
+
75
+ // Track client for this session
76
+ if (!this.clients.has(sessionId)) {
77
+ this.clients.set(sessionId, new Set());
78
+ }
79
+ this.clients.get(sessionId).add(ws);
80
+
81
+ console.log(`Client connected to session ${sessionId} stream`);
82
+
83
+ // Send welcome message
84
+ this.sendToClient(ws, {
85
+ type: 'connected',
86
+ sessionId,
87
+ timestamp: Date.now(),
88
+ message: 'Connected to browser event stream'
89
+ });
90
+
91
+ // Handle client messages (optional commands)
92
+ ws.on('message', (data) => {
93
+ this.handleClientMessage(ws, sessionId, data);
94
+ });
95
+
96
+ // Handle disconnect
97
+ ws.on('close', () => {
98
+ const clients = this.clients.get(sessionId);
99
+ if (clients) {
100
+ clients.delete(ws);
101
+ if (clients.size === 0) {
102
+ this.clients.delete(sessionId);
103
+ }
104
+ }
105
+ console.log(`Client disconnected from session ${sessionId} stream`);
106
+ });
107
+
108
+ // Handle errors
109
+ ws.on('error', (error) => {
110
+ console.error(`WebSocket error for session ${sessionId}:`, error.message);
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Extract session ID from WebSocket URL
116
+ * @private
117
+ */
118
+ extractSessionId(url) {
119
+ // URL format: /browser-control/stream/sess_abc123?query=params
120
+ const match = url.match(/\/browser-control\/stream\/([^?]+)/);
121
+ return match ? match[1] : null;
122
+ }
123
+
124
+ /**
125
+ * Handle messages from clients
126
+ * @private
127
+ */
128
+ handleClientMessage(ws, sessionId, data) {
129
+ try {
130
+ const message = JSON.parse(data.toString());
131
+
132
+ // Optional: Support simple commands via WebSocket
133
+ if (message.action === 'ping') {
134
+ this.sendToClient(ws, {
135
+ type: 'pong',
136
+ timestamp: Date.now()
137
+ });
138
+ }
139
+ } catch (error) {
140
+ console.error('Failed to parse client message:', error.message);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Attach listeners to session manager events
146
+ * @private
147
+ */
148
+ attachSessionListeners() {
149
+ // Console messages
150
+ this.sessionManager.on('console', (sessionId, data) => {
151
+ this.broadcast(sessionId, {
152
+ type: 'console',
153
+ level: data.level,
154
+ text: data.text,
155
+ timestamp: data.timestamp
156
+ });
157
+ });
158
+
159
+ // Page errors
160
+ this.sessionManager.on('error', (sessionId, data) => {
161
+ this.broadcast(sessionId, {
162
+ type: 'error',
163
+ message: data.message,
164
+ stack: data.stack,
165
+ timestamp: data.timestamp
166
+ });
167
+ });
168
+
169
+ // Network requests
170
+ this.sessionManager.on('network', (sessionId, data) => {
171
+ this.broadcast(sessionId, {
172
+ type: 'network',
173
+ method: data.method,
174
+ url: data.url,
175
+ status: data.status,
176
+ timestamp: data.timestamp
177
+ });
178
+ });
179
+
180
+ // Page navigation
181
+ this.sessionManager.on('navigation', (sessionId, data) => {
182
+ this.broadcast(sessionId, {
183
+ type: 'navigation',
184
+ url: data.url,
185
+ timestamp: data.timestamp
186
+ });
187
+ });
188
+
189
+ // Session events
190
+ this.sessionManager.on('session-created', ({ sessionId }) => {
191
+ this.broadcast(sessionId, {
192
+ type: 'session-event',
193
+ event: 'created',
194
+ sessionId,
195
+ timestamp: Date.now()
196
+ });
197
+ });
198
+
199
+ this.sessionManager.on('session-cleaned', ({ sessionId }) => {
200
+ this.broadcast(sessionId, {
201
+ type: 'session-event',
202
+ event: 'cleaned',
203
+ sessionId,
204
+ timestamp: Date.now()
205
+ });
206
+
207
+ // Close all client connections for this session
208
+ const clients = this.clients.get(sessionId);
209
+ if (clients) {
210
+ clients.forEach(ws => {
211
+ ws.close(1000, 'Session closed');
212
+ });
213
+ this.clients.delete(sessionId);
214
+ }
215
+ });
216
+
217
+ this.sessionManager.on('session-crashed', ({ sessionId }) => {
218
+ this.broadcast(sessionId, {
219
+ type: 'session-event',
220
+ event: 'crashed',
221
+ sessionId,
222
+ timestamp: Date.now()
223
+ });
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Broadcast a message to all clients watching a session
229
+ * @param {string} sessionId - Session ID
230
+ * @param {Object} message - Message to broadcast
231
+ */
232
+ broadcast(sessionId, message) {
233
+ const clients = this.clients.get(sessionId);
234
+ if (!clients || clients.size === 0) {
235
+ return;
236
+ }
237
+
238
+ const payload = JSON.stringify(message);
239
+
240
+ clients.forEach(client => {
241
+ if (client.readyState === WebSocket.OPEN) {
242
+ client.send(payload);
243
+ }
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Send a message to a specific client
249
+ * @private
250
+ */
251
+ sendToClient(ws, message) {
252
+ if (ws.readyState === WebSocket.OPEN) {
253
+ ws.send(JSON.stringify(message));
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Get count of active connections per session
259
+ * @returns {Map} - sessionId -> client count
260
+ */
261
+ getConnectionCounts() {
262
+ const counts = new Map();
263
+ for (const [sessionId, clients] of this.clients.entries()) {
264
+ counts.set(sessionId, clients.size);
265
+ }
266
+ return counts;
267
+ }
268
+
269
+ /**
270
+ * Close all WebSocket connections
271
+ */
272
+ closeAll() {
273
+ this.wss.clients.forEach(client => {
274
+ client.close(1000, 'Server shutting down');
275
+ });
276
+ this.clients.clear();
277
+ }
278
+
279
+ /**
280
+ * Shutdown the WebSocket server
281
+ * @returns {Promise<void>}
282
+ */
283
+ async shutdown() {
284
+ return new Promise((resolve) => {
285
+ this.closeAll();
286
+ this.wss.close(() => {
287
+ console.log('Browser Event Stream shut down');
288
+ resolve();
289
+ });
290
+ });
291
+ }
292
+ }
293
+
294
+ module.exports = BrowserEventStream;
@@ -0,0 +1,304 @@
1
+ const EventEmitter = require('events');
2
+ const puppeteer = require('puppeteer');
3
+
4
+ /**
5
+ * BrowserSessionManager
6
+ *
7
+ * Manages persistent Puppeteer browser instances for AI agent verification.
8
+ * Each session represents an isolated browser with its own page and event listeners.
9
+ *
10
+ * Features:
11
+ * - Create/manage multiple browser sessions
12
+ * - Track session activity with automatic timeout
13
+ * - Capture console logs, errors, and network events
14
+ * - Handle browser crashes with recovery logic
15
+ */
16
+ class BrowserSessionManager extends EventEmitter {
17
+ constructor(options = {}) {
18
+ super();
19
+ this.sessions = new Map(); // sessionId -> { browser, page, metadata }
20
+ this.sessionTimeout = options.sessionTimeout || 30 * 60 * 1000; // 30 minutes default
21
+ this.maxSessions = options.maxSessions || 5;
22
+
23
+ // Start cleanup interval to remove inactive sessions
24
+ this.cleanupInterval = setInterval(() => {
25
+ this.cleanupInactiveSessions();
26
+ }, 60000); // Check every minute
27
+ }
28
+
29
+ /**
30
+ * Create a new browser session
31
+ * @param {Object} options - Configuration options
32
+ * @param {boolean} options.headless - Run in headless mode (default: true)
33
+ * @param {number} options.width - Browser width (default: 1280)
34
+ * @param {number} options.height - Browser height (default: 720)
35
+ * @param {number} options.slowMo - Slow down operations by N milliseconds
36
+ * @returns {Promise<Object>} - { sessionId, page, browser }
37
+ */
38
+ async createSession(options = {}) {
39
+ // Check session limit
40
+ if (this.sessions.size >= this.maxSessions) {
41
+ throw new Error(`Maximum number of sessions (${this.maxSessions}) reached`);
42
+ }
43
+
44
+ const headless = options.headless ?? (process.env.BROWSER_HEADLESS !== 'false');
45
+ const slowMo = options.slowMo || parseInt(process.env.BROWSER_SLOW_MO) || 0;
46
+
47
+ try {
48
+ // Launch Puppeteer browser
49
+ const browser = await puppeteer.launch({
50
+ headless: headless ? 'new' : false,
51
+ args: [
52
+ '--no-sandbox',
53
+ '--disable-setuid-sandbox',
54
+ '--disable-dev-shm-usage'
55
+ ],
56
+ defaultViewport: {
57
+ width: options.width || 1280,
58
+ height: options.height || 720
59
+ },
60
+ slowMo
61
+ });
62
+
63
+ // Create new page
64
+ const page = await browser.newPage();
65
+
66
+ // Generate unique session ID
67
+ const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
68
+
69
+ // Set up event listeners
70
+ this.attachPageListeners(sessionId, page);
71
+
72
+ // Handle browser disconnect/crash
73
+ browser.on('disconnected', () => {
74
+ this.handleBrowserCrash(sessionId);
75
+ });
76
+
77
+ // Store session
78
+ const sessionData = {
79
+ browser,
80
+ page,
81
+ createdAt: Date.now(),
82
+ lastActivity: Date.now(),
83
+ options,
84
+ crashed: false
85
+ };
86
+ this.sessions.set(sessionId, sessionData);
87
+
88
+ this.emit('session-created', { sessionId, options });
89
+
90
+ return { sessionId, page, browser };
91
+ } catch (error) {
92
+ throw new Error(`Failed to create browser session: ${error.message}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Attach event listeners to page for logging and debugging
98
+ * @private
99
+ */
100
+ attachPageListeners(sessionId, page) {
101
+ // Console messages
102
+ page.on('console', msg => {
103
+ const data = {
104
+ level: msg.type(),
105
+ text: msg.text(),
106
+ timestamp: Date.now()
107
+ };
108
+ this.emit('console', sessionId, data);
109
+ });
110
+
111
+ // Page errors
112
+ page.on('pageerror', error => {
113
+ const data = {
114
+ message: error.message,
115
+ stack: error.stack,
116
+ timestamp: Date.now()
117
+ };
118
+ this.emit('error', sessionId, data);
119
+ });
120
+
121
+ // Network requests
122
+ page.on('requestfinished', request => {
123
+ const response = request.response();
124
+ if (response) {
125
+ const data = {
126
+ method: request.method(),
127
+ url: request.url(),
128
+ status: response.status(),
129
+ timestamp: Date.now()
130
+ };
131
+ this.emit('network', sessionId, data);
132
+ }
133
+ });
134
+
135
+ // Page navigation
136
+ page.on('framenavigated', frame => {
137
+ if (frame === page.mainFrame()) {
138
+ const data = {
139
+ url: frame.url(),
140
+ timestamp: Date.now()
141
+ };
142
+ this.emit('navigation', sessionId, data);
143
+ }
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Get an existing session and update its last activity time
149
+ * @param {string} sessionId - The session ID
150
+ * @returns {Object} - Session data
151
+ * @throws {Error} - If session not found
152
+ */
153
+ getSession(sessionId) {
154
+ const session = this.sessions.get(sessionId);
155
+ if (!session) {
156
+ throw new Error(`Session ${sessionId} not found or expired`);
157
+ }
158
+ if (session.crashed) {
159
+ throw new Error(`Session ${sessionId} has crashed`);
160
+ }
161
+
162
+ // Update last activity
163
+ session.lastActivity = Date.now();
164
+ return session;
165
+ }
166
+
167
+ /**
168
+ * Get all active sessions
169
+ * @returns {Array} - Array of session info objects
170
+ */
171
+ getAllSessions() {
172
+ const sessions = [];
173
+ for (const [sessionId, data] of this.sessions.entries()) {
174
+ sessions.push({
175
+ sessionId,
176
+ createdAt: data.createdAt,
177
+ lastActivity: data.lastActivity,
178
+ uptime: Date.now() - data.createdAt,
179
+ crashed: data.crashed,
180
+ pageUrl: data.page?.url?.() || 'unknown'
181
+ });
182
+ }
183
+ return sessions;
184
+ }
185
+
186
+ /**
187
+ * Check if a session exists
188
+ * @param {string} sessionId - The session ID
189
+ * @returns {boolean}
190
+ */
191
+ hasSession(sessionId) {
192
+ return this.sessions.has(sessionId);
193
+ }
194
+
195
+ /**
196
+ * Clean up a specific session
197
+ * @param {string} sessionId - The session ID
198
+ * @returns {Promise<boolean>} - True if session was cleaned up
199
+ */
200
+ async cleanup(sessionId) {
201
+ const session = this.sessions.get(sessionId);
202
+ if (!session) {
203
+ return false;
204
+ }
205
+
206
+ try {
207
+ // Close page first
208
+ if (session.page && !session.page.isClosed()) {
209
+ await session.page.close();
210
+ }
211
+
212
+ // Close browser
213
+ if (session.browser && session.browser.isConnected()) {
214
+ await session.browser.close();
215
+ }
216
+ } catch (error) {
217
+ console.error(`Error cleaning up session ${sessionId}:`, error.message);
218
+ } finally {
219
+ // Remove from sessions map
220
+ this.sessions.delete(sessionId);
221
+ this.emit('session-cleaned', { sessionId });
222
+ }
223
+
224
+ return true;
225
+ }
226
+
227
+ /**
228
+ * Clean up all sessions
229
+ * @returns {Promise<void>}
230
+ */
231
+ async cleanupAll() {
232
+ const sessionIds = Array.from(this.sessions.keys());
233
+ await Promise.all(sessionIds.map(id => this.cleanup(id)));
234
+ }
235
+
236
+ /**
237
+ * Clean up inactive sessions based on timeout
238
+ * @private
239
+ */
240
+ cleanupInactiveSessions() {
241
+ const now = Date.now();
242
+
243
+ // Collect inactive session IDs first to avoid mutating this.sessions during iteration
244
+ const inactiveSessionIds = [];
245
+ for (const [sessionId, session] of this.sessions.entries()) {
246
+ if (now - session.lastActivity > this.sessionTimeout) {
247
+ inactiveSessionIds.push(sessionId);
248
+ }
249
+ }
250
+
251
+ // Clean up collected sessions outside the iteration
252
+ if (inactiveSessionIds.length > 0) {
253
+ console.log(`Cleaning up ${inactiveSessionIds.length} inactive session(s)`);
254
+
255
+ Promise.allSettled(
256
+ inactiveSessionIds.map(sessionId => this.cleanup(sessionId))
257
+ ).then(results => {
258
+ results.forEach((result, index) => {
259
+ if (result.status === 'rejected') {
260
+ console.error(`Failed to cleanup session ${inactiveSessionIds[index]}:`, result.reason);
261
+ }
262
+ });
263
+ });
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Handle browser crash
269
+ * @private
270
+ */
271
+ handleBrowserCrash(sessionId) {
272
+ const session = this.sessions.get(sessionId);
273
+ if (session) {
274
+ session.crashed = true;
275
+ this.emit('session-crashed', { sessionId });
276
+ console.error(`Browser session ${sessionId} crashed`);
277
+
278
+ // Clean up after a short delay
279
+ setTimeout(() => {
280
+ this.cleanup(sessionId).catch(err => {
281
+ console.error(`Failed to cleanup crashed session ${sessionId}:`, err);
282
+ });
283
+ }, 5000);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Shutdown the session manager and clean up all sessions
289
+ * @returns {Promise<void>}
290
+ */
291
+ async shutdown() {
292
+ // Stop cleanup interval
293
+ if (this.cleanupInterval) {
294
+ clearInterval(this.cleanupInterval);
295
+ }
296
+
297
+ // Clean up all sessions
298
+ await this.cleanupAll();
299
+
300
+ this.emit('shutdown');
301
+ }
302
+ }
303
+
304
+ module.exports = BrowserSessionManager;