chrome-cdp-cli 1.5.0 → 1.6.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.
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FileSystemSecurity = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const logger_1 = require("../../utils/logger");
41
+ class FileSystemSecurity {
42
+ constructor(config) {
43
+ this.logger = (0, logger_1.createLogger)({ component: 'FileSystemSecurity' });
44
+ const homeDir = os.homedir();
45
+ const configDir = path.join(homeDir, '.chrome-cdp-cli');
46
+ this.config = {
47
+ allowedDirectories: [
48
+ configDir,
49
+ path.join(configDir, 'logs'),
50
+ path.join(configDir, 'config'),
51
+ '/tmp',
52
+ os.tmpdir()
53
+ ],
54
+ configDirectory: configDir,
55
+ logDirectory: path.join(configDir, 'logs'),
56
+ enablePermissionChecks: true,
57
+ enableDataSanitization: true,
58
+ maxFileSize: 100 * 1024 * 1024,
59
+ ...config
60
+ };
61
+ this.allowedPaths = new Set();
62
+ this.initializeAllowedPaths();
63
+ this.ensureDirectoriesExist();
64
+ this.logger.info('File system security initialized', {
65
+ config: this.sanitizeConfigForLogging(this.config)
66
+ });
67
+ }
68
+ initializeAllowedPaths() {
69
+ for (const dir of this.config.allowedDirectories) {
70
+ try {
71
+ const resolvedPath = path.resolve(dir);
72
+ this.allowedPaths.add(resolvedPath);
73
+ this.allowedPaths.add(path.join(resolvedPath, 'proxy'));
74
+ this.allowedPaths.add(path.join(resolvedPath, 'temp'));
75
+ }
76
+ catch (error) {
77
+ this.logger.warn(`Failed to resolve allowed directory: ${dir}`, { error });
78
+ }
79
+ }
80
+ }
81
+ ensureDirectoriesExist() {
82
+ const requiredDirs = [
83
+ this.config.configDirectory,
84
+ this.config.logDirectory
85
+ ];
86
+ for (const dir of requiredDirs) {
87
+ try {
88
+ if (!fs.existsSync(dir)) {
89
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
90
+ this.logger.info(`Created directory with secure permissions: ${dir}`);
91
+ }
92
+ else {
93
+ this.checkAndFixDirectoryPermissions(dir);
94
+ }
95
+ }
96
+ catch (error) {
97
+ this.logger.error(`Failed to create/secure directory: ${dir}`, error);
98
+ }
99
+ }
100
+ }
101
+ isPathAllowed(filePath) {
102
+ try {
103
+ const resolvedPath = path.resolve(filePath);
104
+ const normalizedPath = path.normalize(resolvedPath);
105
+ for (const allowedPath of this.allowedPaths) {
106
+ if (normalizedPath.startsWith(allowedPath)) {
107
+ return true;
108
+ }
109
+ }
110
+ this.logger.logSecurityEvent('unauthorized_path_access', 'Attempted access to unauthorized path', {
111
+ requestedPath: filePath,
112
+ resolvedPath,
113
+ allowedPaths: Array.from(this.allowedPaths)
114
+ });
115
+ return false;
116
+ }
117
+ catch (error) {
118
+ this.logger.logSecurityEvent('path_validation_error', 'Error validating file path', {
119
+ requestedPath: filePath,
120
+ error: error.message
121
+ });
122
+ return false;
123
+ }
124
+ }
125
+ async readFileSecurely(filePath) {
126
+ if (!this.isPathAllowed(filePath)) {
127
+ throw new Error(`Access denied: Path not allowed - ${filePath}`);
128
+ }
129
+ try {
130
+ const stats = await fs.promises.stat(filePath);
131
+ if (stats.size > this.config.maxFileSize) {
132
+ throw new Error(`File too large: ${stats.size} bytes (max: ${this.config.maxFileSize})`);
133
+ }
134
+ const content = await fs.promises.readFile(filePath, 'utf8');
135
+ this.logger.debug('Secure file read completed', {
136
+ filePath: this.sanitizePath(filePath),
137
+ fileSize: stats.size
138
+ });
139
+ return this.config.enableDataSanitization ? this.sanitizeFileContent(content) : content;
140
+ }
141
+ catch (error) {
142
+ this.logger.logSecurityEvent('file_read_error', 'Secure file read failed', {
143
+ filePath: this.sanitizePath(filePath),
144
+ error: error.message
145
+ });
146
+ throw error;
147
+ }
148
+ }
149
+ async writeFileSecurely(filePath, content) {
150
+ if (!this.isPathAllowed(filePath)) {
151
+ throw new Error(`Access denied: Path not allowed - ${filePath}`);
152
+ }
153
+ try {
154
+ const sanitizedContent = this.config.enableDataSanitization
155
+ ? this.sanitizeFileContent(content)
156
+ : content;
157
+ const contentSize = Buffer.byteLength(sanitizedContent, 'utf8');
158
+ if (contentSize > this.config.maxFileSize) {
159
+ throw new Error(`Content too large: ${contentSize} bytes (max: ${this.config.maxFileSize})`);
160
+ }
161
+ const dir = path.dirname(filePath);
162
+ if (!fs.existsSync(dir)) {
163
+ await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
164
+ }
165
+ await fs.promises.writeFile(filePath, sanitizedContent, {
166
+ encoding: 'utf8',
167
+ mode: 0o600
168
+ });
169
+ this.logger.debug('Secure file write completed', {
170
+ filePath: this.sanitizePath(filePath),
171
+ contentSize
172
+ });
173
+ }
174
+ catch (error) {
175
+ this.logger.logSecurityEvent('file_write_error', 'Secure file write failed', {
176
+ filePath: this.sanitizePath(filePath),
177
+ error: error.message
178
+ });
179
+ throw error;
180
+ }
181
+ }
182
+ async checkConfigurationSecurity(configPath) {
183
+ const issues = [];
184
+ const recommendations = [];
185
+ try {
186
+ if (!this.isPathAllowed(configPath)) {
187
+ issues.push('Configuration file is not in an allowed directory');
188
+ recommendations.push(`Move configuration to: ${this.config.configDirectory}`);
189
+ return { isSecure: false, issues, recommendations };
190
+ }
191
+ if (!fs.existsSync(configPath)) {
192
+ issues.push('Configuration file does not exist');
193
+ recommendations.push('Create configuration file with secure permissions');
194
+ return { isSecure: false, issues, recommendations };
195
+ }
196
+ const stats = await fs.promises.stat(configPath);
197
+ const mode = stats.mode & parseInt('777', 8);
198
+ if (mode > parseInt('644', 8)) {
199
+ issues.push(`Configuration file has overly permissive permissions: ${mode.toString(8)}`);
200
+ recommendations.push('Set file permissions to 600 (owner read/write only)');
201
+ }
202
+ if (mode & parseInt('004', 8)) {
203
+ issues.push('Configuration file is world-readable');
204
+ recommendations.push('Remove world-read permissions');
205
+ }
206
+ if (mode & parseInt('020', 8)) {
207
+ issues.push('Configuration file is group-writable');
208
+ recommendations.push('Remove group-write permissions');
209
+ }
210
+ if (stats.size > 1024 * 1024) {
211
+ issues.push('Configuration file is unusually large');
212
+ recommendations.push('Review configuration file for unnecessary content');
213
+ }
214
+ if (process.getuid && process.getuid() !== 0) {
215
+ const uid = process.getuid();
216
+ if (stats.uid !== uid) {
217
+ issues.push('Configuration file is not owned by current user');
218
+ recommendations.push('Change file ownership to current user');
219
+ }
220
+ }
221
+ const isSecure = issues.length === 0;
222
+ this.logger.info('Configuration security check completed', {
223
+ configPath: this.sanitizePath(configPath),
224
+ isSecure,
225
+ issueCount: issues.length
226
+ });
227
+ return { isSecure, issues, recommendations };
228
+ }
229
+ catch (error) {
230
+ this.logger.logSecurityEvent('config_security_check_error', 'Configuration security check failed', {
231
+ configPath: this.sanitizePath(configPath),
232
+ error: error.message
233
+ });
234
+ issues.push(`Security check failed: ${error.message}`);
235
+ return { isSecure: false, issues, recommendations };
236
+ }
237
+ }
238
+ async fixConfigurationPermissions(configPath) {
239
+ if (!this.isPathAllowed(configPath)) {
240
+ throw new Error(`Access denied: Path not allowed - ${configPath}`);
241
+ }
242
+ try {
243
+ await fs.promises.chmod(configPath, 0o600);
244
+ this.logger.info('Configuration file permissions fixed', {
245
+ configPath: this.sanitizePath(configPath),
246
+ newPermissions: '600'
247
+ });
248
+ }
249
+ catch (error) {
250
+ this.logger.logSecurityEvent('permission_fix_error', 'Failed to fix configuration permissions', {
251
+ configPath: this.sanitizePath(configPath),
252
+ error: error.message
253
+ });
254
+ throw error;
255
+ }
256
+ }
257
+ sanitizeFileContent(content) {
258
+ let sanitized = content;
259
+ const sensitivePatterns = [
260
+ /['"](api[_-]?key|token|secret|password)['"]\s*:\s*['"][^'"]+['"]/gi,
261
+ /https?:\/\/[^:]+:[^@]+@[^\s]+/gi,
262
+ /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(RSA\s+)?PRIVATE\s+KEY-----/gi,
263
+ /eyJ[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/g,
264
+ /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
265
+ /\b\d{3}-\d{2}-\d{4}\b/g
266
+ ];
267
+ for (const pattern of sensitivePatterns) {
268
+ sanitized = sanitized.replace(pattern, (match) => {
269
+ this.logger.logSecurityEvent('sensitive_data_sanitized', 'Sensitive data pattern detected and sanitized', {
270
+ patternLength: match.length,
271
+ patternStart: match.substring(0, 10) + '...'
272
+ });
273
+ return '[REDACTED]';
274
+ });
275
+ }
276
+ return sanitized;
277
+ }
278
+ sanitizePath(filePath) {
279
+ const homeDir = os.homedir();
280
+ return filePath.replace(homeDir, '~');
281
+ }
282
+ sanitizeConfigForLogging(config) {
283
+ return {
284
+ ...config,
285
+ allowedDirectories: config.allowedDirectories.map(dir => this.sanitizePath(dir)),
286
+ configDirectory: this.sanitizePath(config.configDirectory),
287
+ logDirectory: this.sanitizePath(config.logDirectory)
288
+ };
289
+ }
290
+ checkAndFixDirectoryPermissions(dirPath) {
291
+ try {
292
+ const stats = fs.statSync(dirPath);
293
+ const mode = stats.mode & parseInt('777', 8);
294
+ if (mode !== parseInt('700', 8)) {
295
+ fs.chmodSync(dirPath, 0o700);
296
+ this.logger.info('Fixed directory permissions', {
297
+ directory: this.sanitizePath(dirPath),
298
+ oldMode: mode.toString(8),
299
+ newMode: '700'
300
+ });
301
+ }
302
+ }
303
+ catch (error) {
304
+ this.logger.warn('Failed to check/fix directory permissions', {
305
+ directory: this.sanitizePath(dirPath),
306
+ error: error.message
307
+ });
308
+ }
309
+ }
310
+ getConfig() {
311
+ return { ...this.config };
312
+ }
313
+ updateConfig(newConfig) {
314
+ this.config = { ...this.config, ...newConfig };
315
+ this.allowedPaths.clear();
316
+ this.initializeAllowedPaths();
317
+ this.ensureDirectoriesExist();
318
+ this.logger.info('File system security configuration updated', {
319
+ config: this.sanitizeConfigForLogging(this.config)
320
+ });
321
+ }
322
+ addAllowedDirectory(directory) {
323
+ try {
324
+ const resolvedPath = path.resolve(directory);
325
+ this.allowedPaths.add(resolvedPath);
326
+ this.config.allowedDirectories.push(directory);
327
+ this.logger.info('Added allowed directory', {
328
+ directory: this.sanitizePath(directory),
329
+ resolvedPath: this.sanitizePath(resolvedPath)
330
+ });
331
+ }
332
+ catch (error) {
333
+ this.logger.error('Failed to add allowed directory', error, {
334
+ data: { directory: this.sanitizePath(directory) }
335
+ });
336
+ }
337
+ }
338
+ removeAllowedDirectory(directory) {
339
+ try {
340
+ const resolvedPath = path.resolve(directory);
341
+ this.allowedPaths.delete(resolvedPath);
342
+ const index = this.config.allowedDirectories.indexOf(directory);
343
+ if (index > -1) {
344
+ this.config.allowedDirectories.splice(index, 1);
345
+ }
346
+ this.logger.info('Removed allowed directory', {
347
+ directory: this.sanitizePath(directory),
348
+ resolvedPath: this.sanitizePath(resolvedPath)
349
+ });
350
+ }
351
+ catch (error) {
352
+ this.logger.error('Failed to remove allowed directory', error, {
353
+ data: { directory: this.sanitizePath(directory) }
354
+ });
355
+ }
356
+ }
357
+ }
358
+ exports.FileSystemSecurity = FileSystemSecurity;
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HealthMonitor = void 0;
4
+ const logger_1 = require("../../utils/logger");
5
+ class HealthMonitor {
6
+ constructor(connectionPool) {
7
+ this.healthChecks = new Map();
8
+ this.connectionMetrics = new Map();
9
+ this.isRunning = false;
10
+ this.healthCheckTimeout = 5000;
11
+ this.maxConsecutiveFailures = 3;
12
+ this.connectionPool = connectionPool;
13
+ this.logger = new logger_1.Logger();
14
+ }
15
+ start(intervalMs = 30000) {
16
+ if (this.checkInterval) {
17
+ this.logger.warn('Health monitoring already running, stopping previous instance');
18
+ this.stop();
19
+ }
20
+ this.logger.info(`Starting health monitoring with ${intervalMs}ms interval`);
21
+ this.isRunning = true;
22
+ this.performHealthChecks().catch(error => {
23
+ this.logger.error('Initial health check failed:', error);
24
+ });
25
+ this.checkInterval = setInterval(async () => {
26
+ await this.performHealthChecks();
27
+ }, intervalMs);
28
+ }
29
+ stop() {
30
+ if (this.checkInterval) {
31
+ clearInterval(this.checkInterval);
32
+ this.checkInterval = undefined;
33
+ this.isRunning = false;
34
+ this.logger.info('Health monitoring stopped');
35
+ }
36
+ }
37
+ isMonitoring() {
38
+ return this.isRunning;
39
+ }
40
+ async checkConnection(connectionId) {
41
+ const startTime = Date.now();
42
+ try {
43
+ const connectionInfo = this.connectionPool.getConnectionInfo(connectionId);
44
+ if (!connectionInfo) {
45
+ const result = {
46
+ connectionId,
47
+ isHealthy: false,
48
+ lastCheck: Date.now(),
49
+ errorCount: 0,
50
+ lastError: 'Connection not found'
51
+ };
52
+ this.healthChecks.set(connectionId, result);
53
+ return result;
54
+ }
55
+ const isHealthy = await Promise.race([
56
+ this.connectionPool.healthCheck(connectionId),
57
+ new Promise((_, reject) => {
58
+ setTimeout(() => reject(new Error('Health check timeout')), this.healthCheckTimeout);
59
+ })
60
+ ]);
61
+ const now = Date.now();
62
+ const existingResult = this.healthChecks.get(connectionId);
63
+ const result = {
64
+ connectionId,
65
+ isHealthy,
66
+ lastCheck: now,
67
+ errorCount: isHealthy ? 0 : (existingResult?.errorCount || 0) + 1,
68
+ lastError: isHealthy ? undefined : existingResult?.lastError || 'Health check failed'
69
+ };
70
+ this.healthChecks.set(connectionId, result);
71
+ this.updateConnectionMetrics(connectionId, connectionInfo, now - startTime);
72
+ if (!isHealthy) {
73
+ this.logger.warn(`Health check failed for connection ${connectionId} (${result.errorCount} consecutive failures)`);
74
+ }
75
+ else if (existingResult && !existingResult.isHealthy) {
76
+ this.logger.info(`Connection ${connectionId} recovered and is now healthy`);
77
+ }
78
+ return result;
79
+ }
80
+ catch (error) {
81
+ const now = Date.now();
82
+ const existingResult = this.healthChecks.get(connectionId);
83
+ const errorMessage = error instanceof Error ? error.message : 'Unknown health check error';
84
+ const result = {
85
+ connectionId,
86
+ isHealthy: false,
87
+ lastCheck: now,
88
+ errorCount: (existingResult?.errorCount || 0) + 1,
89
+ lastError: errorMessage
90
+ };
91
+ this.healthChecks.set(connectionId, result);
92
+ this.logger.error(`Health check error for connection ${connectionId}:`, error);
93
+ return result;
94
+ }
95
+ }
96
+ async attemptReconnection(connectionId) {
97
+ this.logger.info(`Attempting reconnection for connection ${connectionId}`);
98
+ try {
99
+ this.incrementReconnectionCount(connectionId);
100
+ const success = await this.connectionPool.reconnect(connectionId);
101
+ if (success) {
102
+ const result = {
103
+ connectionId,
104
+ isHealthy: true,
105
+ lastCheck: Date.now(),
106
+ errorCount: 0
107
+ };
108
+ this.healthChecks.set(connectionId, result);
109
+ this.logger.info(`Reconnection successful for connection ${connectionId}`);
110
+ }
111
+ else {
112
+ const existingResult = this.healthChecks.get(connectionId);
113
+ const result = {
114
+ connectionId,
115
+ isHealthy: false,
116
+ lastCheck: Date.now(),
117
+ errorCount: (existingResult?.errorCount || 0) + 1,
118
+ lastError: 'Reconnection failed'
119
+ };
120
+ this.healthChecks.set(connectionId, result);
121
+ this.logger.warn(`Reconnection failed for connection ${connectionId}`);
122
+ }
123
+ return success;
124
+ }
125
+ catch (error) {
126
+ const existingResult = this.healthChecks.get(connectionId);
127
+ const errorMessage = error instanceof Error ? error.message : 'Unknown reconnection error';
128
+ const result = {
129
+ connectionId,
130
+ isHealthy: false,
131
+ lastCheck: Date.now(),
132
+ errorCount: (existingResult?.errorCount || 0) + 1,
133
+ lastError: `Reconnection error: ${errorMessage}`
134
+ };
135
+ this.healthChecks.set(connectionId, result);
136
+ this.logger.error(`Reconnection error for connection ${connectionId}:`, error);
137
+ return false;
138
+ }
139
+ }
140
+ getHealthStatus(connectionId) {
141
+ return this.healthChecks.get(connectionId) || null;
142
+ }
143
+ getAllHealthStatuses() {
144
+ return Array.from(this.healthChecks.values());
145
+ }
146
+ getConnectionMetrics(connectionId) {
147
+ return this.connectionMetrics.get(connectionId) || null;
148
+ }
149
+ getAllConnectionMetrics() {
150
+ return Array.from(this.connectionMetrics.values());
151
+ }
152
+ getHealthStatistics() {
153
+ const healthStatuses = this.getAllHealthStatuses();
154
+ const metrics = this.getAllConnectionMetrics();
155
+ const totalConnections = healthStatuses.length;
156
+ const healthyConnections = healthStatuses.filter(h => h.isHealthy).length;
157
+ const unhealthyConnections = totalConnections - healthyConnections;
158
+ const recentMetrics = metrics.filter(m => m.lastActivity > Date.now() - 300000);
159
+ const averageResponseTime = recentMetrics.length > 0
160
+ ? recentMetrics.reduce((sum, m) => sum + (m.lastActivity || 0), 0) / recentMetrics.length
161
+ : 0;
162
+ const totalHealthChecks = healthStatuses.reduce((sum, h) => sum + (h.errorCount || 0), 0) + healthyConnections;
163
+ return {
164
+ totalConnections,
165
+ healthyConnections,
166
+ unhealthyConnections,
167
+ averageResponseTime,
168
+ totalHealthChecks
169
+ };
170
+ }
171
+ async forceHealthCheckAll() {
172
+ const connections = this.connectionPool.getAllConnections();
173
+ const results = [];
174
+ for (const connection of connections) {
175
+ try {
176
+ const result = await this.checkConnection(connection.id);
177
+ results.push(result);
178
+ }
179
+ catch (error) {
180
+ this.logger.error(`Force health check failed for connection ${connection.id}:`, error);
181
+ }
182
+ }
183
+ return results;
184
+ }
185
+ cleanupHealthData(connectionId) {
186
+ this.healthChecks.delete(connectionId);
187
+ this.connectionMetrics.delete(connectionId);
188
+ this.logger.debug(`Cleaned up health data for connection ${connectionId}`);
189
+ }
190
+ async performHealthChecks() {
191
+ if (!this.isRunning) {
192
+ return;
193
+ }
194
+ const connections = this.connectionPool.getAllConnections();
195
+ this.logger.debug(`Performing health checks for ${connections.length} connections`);
196
+ const healthCheckPromises = connections.map(async (connection) => {
197
+ try {
198
+ const result = await this.checkConnection(connection.id);
199
+ if (!result.isHealthy && result.errorCount >= this.maxConsecutiveFailures) {
200
+ this.logger.warn(`Connection ${connection.id} has ${result.errorCount} consecutive failures, attempting reconnection`);
201
+ await this.attemptReconnection(connection.id);
202
+ }
203
+ }
204
+ catch (error) {
205
+ this.logger.error(`Health check error for connection ${connection.id}:`, error);
206
+ }
207
+ });
208
+ await Promise.allSettled(healthCheckPromises);
209
+ const activeConnectionIds = new Set(connections.map(c => c.id));
210
+ for (const connectionId of this.healthChecks.keys()) {
211
+ if (!activeConnectionIds.has(connectionId)) {
212
+ this.cleanupHealthData(connectionId);
213
+ }
214
+ }
215
+ const stats = this.getHealthStatistics();
216
+ if (stats.totalConnections > 0) {
217
+ this.logger.debug(`Health check summary: ${stats.healthyConnections}/${stats.totalConnections} healthy connections`);
218
+ }
219
+ }
220
+ updateConnectionMetrics(connectionId, connectionInfo, _responseTime) {
221
+ const existing = this.connectionMetrics.get(connectionId);
222
+ const now = Date.now();
223
+ const metrics = {
224
+ connectionId,
225
+ uptime: now - connectionInfo.createdAt,
226
+ messageCount: existing?.messageCount || 0,
227
+ requestCount: existing?.requestCount || 0,
228
+ clientCount: connectionInfo.clientCount,
229
+ lastActivity: now,
230
+ reconnectionCount: existing?.reconnectionCount || 0
231
+ };
232
+ this.connectionMetrics.set(connectionId, metrics);
233
+ }
234
+ incrementReconnectionCount(connectionId) {
235
+ const existing = this.connectionMetrics.get(connectionId);
236
+ if (existing) {
237
+ existing.reconnectionCount++;
238
+ this.connectionMetrics.set(connectionId, existing);
239
+ }
240
+ }
241
+ }
242
+ exports.HealthMonitor = HealthMonitor;