@ceon-oy/monitor-sdk 1.1.4 → 1.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/README.md CHANGED
@@ -11,6 +11,7 @@ Lightweight client SDK for integrating with the Ceon Monitor service. Provides e
11
11
  - [Error Capture](#error-capture)
12
12
  - [Technology Tracking](#technology-tracking)
13
13
  - [Vulnerability Auditing](#vulnerability-auditing)
14
+ - [SDK-Based Health Checks](#sdk-based-health-checks)
14
15
  - [Security Events](#security-events)
15
16
  - [Framework Examples](#framework-examples)
16
17
  - [Express.js](#expressjs)
@@ -104,6 +105,8 @@ interface MonitorClientConfig {
104
105
  }[];
105
106
  auditTimeoutMs?: number; // Timeout for npm audit command (default: 60000, max: 300000)
106
107
  registryTimeoutMs?: number; // Timeout for npm registry requests (default: 5000, max: 30000)
108
+ healthCheckEnabled?: boolean; // Enable SDK-based health check polling (default: false)
109
+ healthCheckFetchIntervalMs?: number; // Interval to fetch health endpoints in ms (default: 60000)
107
110
  }
108
111
  ```
109
112
 
@@ -413,6 +416,80 @@ setInterval(async () => {
413
416
  }, 24 * 60 * 60 * 1000); // Daily
414
417
  ```
415
418
 
419
+ ### SDK-Based Health Checks
420
+
421
+ The SDK can poll health endpoints that are inside your private network (VPN, internal services) and report the results to Ceon Monitor. This is useful for monitoring services that are not publicly accessible.
422
+
423
+ #### Enable SDK Polling
424
+
425
+ ```typescript
426
+ const monitor = new MonitorClient({
427
+ apiKey: process.env.CEON_MONITOR_API_KEY!,
428
+ endpoint: 'https://monitor.example.com',
429
+ environment: 'production',
430
+
431
+ // Enable SDK-based health check polling
432
+ healthCheckEnabled: true,
433
+
434
+ // Optional: customize fetch interval (default: 60000ms)
435
+ healthCheckFetchIntervalMs: 60000,
436
+ });
437
+ ```
438
+
439
+ With `healthCheckEnabled: true`, the SDK will:
440
+ 1. Fetch assigned health endpoints from the server every 60 seconds (configurable)
441
+ 2. Poll each endpoint locally at its configured interval
442
+ 3. Submit results back to the server in batches
443
+ 4. Automatically handle endpoint additions/removals
444
+
445
+ #### How It Works
446
+
447
+ 1. **Configure in Dashboard**: Create health endpoints in the Ceon Monitor dashboard and set their polling mode to "SDK"
448
+ 2. **SDK Fetches Endpoints**: The SDK periodically fetches the list of endpoints assigned for SDK polling
449
+ 3. **Local Polling**: The SDK polls each endpoint from your server (inside your VPN/private network)
450
+ 4. **Report Results**: Health check results are batched and submitted to Ceon Monitor
451
+
452
+ #### Use Cases
453
+
454
+ - **Internal APIs**: Monitor APIs that are only accessible within your VPN
455
+ - **Database Health**: Check database connection health from inside your network
456
+ - **Microservices**: Monitor internal service-to-service communication
457
+ - **Private Infrastructure**: Any endpoint that's not publicly accessible
458
+
459
+ #### Configuration Options
460
+
461
+ ```typescript
462
+ interface MonitorClientConfig {
463
+ // ... other options
464
+
465
+ /** Enable SDK-based health check polling (default: false) */
466
+ healthCheckEnabled?: boolean;
467
+
468
+ /** Interval to fetch health endpoints from server in ms (default: 60000) */
469
+ healthCheckFetchIntervalMs?: number;
470
+ }
471
+ ```
472
+
473
+ #### Manual Health Check Methods
474
+
475
+ You can also manually perform health checks:
476
+
477
+ ```typescript
478
+ // Fetch endpoints assigned for SDK polling
479
+ const endpoints = await monitor.fetchHealthEndpoints();
480
+
481
+ // Submit health check results
482
+ const results = [
483
+ {
484
+ endpointId: 'clxyz123',
485
+ status: 'HEALTHY',
486
+ statusCode: 200,
487
+ responseTimeMs: 150,
488
+ },
489
+ ];
490
+ await monitor.submitHealthResults(results);
491
+ ```
492
+
416
493
  ### Security Events
417
494
 
418
495
  #### Report Security Event
package/dist/index.d.mts CHANGED
@@ -53,6 +53,10 @@ interface MonitorClientConfig {
53
53
  allowInsecureHttp?: boolean;
54
54
  /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
55
  npmRegistryUrl?: string;
56
+ /** Enable SDK-based health check polling (default: false) */
57
+ healthCheckEnabled?: boolean;
58
+ /** Interval to fetch health endpoints from server in ms (default: 60000) */
59
+ healthCheckFetchIntervalMs?: number;
56
60
  }
57
61
  interface TechnologyItem {
58
62
  name: string;
@@ -150,6 +154,40 @@ interface MultiAuditSummary {
150
154
  }>;
151
155
  totalSummary: VulnerabilitySummary;
152
156
  }
157
+ type HealthStatus = 'HEALTHY' | 'DEGRADED' | 'UNHEALTHY';
158
+ interface SdkHealthEndpoint {
159
+ id: string;
160
+ name: string;
161
+ url: string;
162
+ method: 'GET' | 'POST';
163
+ headers?: Record<string, string>;
164
+ body?: Record<string, unknown>;
165
+ environment: string;
166
+ intervalMs: number;
167
+ timeoutMs: number;
168
+ expectedStatus: number;
169
+ }
170
+ interface SdkHealthResult {
171
+ endpointId: string;
172
+ status: HealthStatus;
173
+ statusCode?: number;
174
+ responseTimeMs?: number;
175
+ errorMessage?: string;
176
+ }
177
+ interface SdkHealthEndpointsResponse {
178
+ success: boolean;
179
+ data: {
180
+ endpoints: SdkHealthEndpoint[];
181
+ pollIntervalMs: number;
182
+ };
183
+ }
184
+ interface SdkHealthResultsResponse {
185
+ success: boolean;
186
+ data: {
187
+ processed: number;
188
+ total: number;
189
+ };
190
+ }
153
191
 
154
192
  declare class MonitorClient {
155
193
  private apiKey;
@@ -182,6 +220,11 @@ declare class MonitorClient {
182
220
  private auditTimeoutMs;
183
221
  private registryTimeoutMs;
184
222
  private npmRegistryUrl;
223
+ private healthCheckEnabled;
224
+ private healthCheckFetchIntervalMs;
225
+ private healthCheckFetchTimer;
226
+ private healthCheckTimers;
227
+ private healthCheckResultsQueue;
185
228
  constructor(config: MonitorClientConfig);
186
229
  /**
187
230
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -382,6 +425,45 @@ declare class MonitorClient {
382
425
  * Yarn audit outputs one JSON object per line (newline-delimited JSON)
383
426
  */
384
427
  private parseYarnAuditOutput;
428
+ /**
429
+ * Fetch health endpoints assigned to this project for SDK polling
430
+ */
431
+ fetchHealthEndpoints(): Promise<SdkHealthEndpoint[]>;
432
+ /**
433
+ * Submit health check results to the server
434
+ */
435
+ submitHealthResults(results: SdkHealthResult[]): Promise<{
436
+ processed: number;
437
+ total: number;
438
+ }>;
439
+ /**
440
+ * Perform a single health check on an endpoint
441
+ */
442
+ private performHealthCheck;
443
+ /**
444
+ * Setup automatic health check polling
445
+ */
446
+ private setupHealthCheckPolling;
447
+ /**
448
+ * Fetch health endpoints and schedule individual checks
449
+ */
450
+ private fetchAndScheduleHealthChecks;
451
+ /**
452
+ * Schedule individual health check for an endpoint
453
+ */
454
+ private scheduleHealthCheck;
455
+ /**
456
+ * Run a health check and queue the result
457
+ */
458
+ private runHealthCheck;
459
+ /**
460
+ * Flush queued health results to the server
461
+ */
462
+ private flushHealthResults;
463
+ /**
464
+ * Stop all health check timers
465
+ */
466
+ private stopHealthCheckTimers;
385
467
  }
386
468
 
387
- export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
469
+ export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.d.ts CHANGED
@@ -53,6 +53,10 @@ interface MonitorClientConfig {
53
53
  allowInsecureHttp?: boolean;
54
54
  /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
55
  npmRegistryUrl?: string;
56
+ /** Enable SDK-based health check polling (default: false) */
57
+ healthCheckEnabled?: boolean;
58
+ /** Interval to fetch health endpoints from server in ms (default: 60000) */
59
+ healthCheckFetchIntervalMs?: number;
56
60
  }
57
61
  interface TechnologyItem {
58
62
  name: string;
@@ -150,6 +154,40 @@ interface MultiAuditSummary {
150
154
  }>;
151
155
  totalSummary: VulnerabilitySummary;
152
156
  }
157
+ type HealthStatus = 'HEALTHY' | 'DEGRADED' | 'UNHEALTHY';
158
+ interface SdkHealthEndpoint {
159
+ id: string;
160
+ name: string;
161
+ url: string;
162
+ method: 'GET' | 'POST';
163
+ headers?: Record<string, string>;
164
+ body?: Record<string, unknown>;
165
+ environment: string;
166
+ intervalMs: number;
167
+ timeoutMs: number;
168
+ expectedStatus: number;
169
+ }
170
+ interface SdkHealthResult {
171
+ endpointId: string;
172
+ status: HealthStatus;
173
+ statusCode?: number;
174
+ responseTimeMs?: number;
175
+ errorMessage?: string;
176
+ }
177
+ interface SdkHealthEndpointsResponse {
178
+ success: boolean;
179
+ data: {
180
+ endpoints: SdkHealthEndpoint[];
181
+ pollIntervalMs: number;
182
+ };
183
+ }
184
+ interface SdkHealthResultsResponse {
185
+ success: boolean;
186
+ data: {
187
+ processed: number;
188
+ total: number;
189
+ };
190
+ }
153
191
 
154
192
  declare class MonitorClient {
155
193
  private apiKey;
@@ -182,6 +220,11 @@ declare class MonitorClient {
182
220
  private auditTimeoutMs;
183
221
  private registryTimeoutMs;
184
222
  private npmRegistryUrl;
223
+ private healthCheckEnabled;
224
+ private healthCheckFetchIntervalMs;
225
+ private healthCheckFetchTimer;
226
+ private healthCheckTimers;
227
+ private healthCheckResultsQueue;
185
228
  constructor(config: MonitorClientConfig);
186
229
  /**
187
230
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -382,6 +425,45 @@ declare class MonitorClient {
382
425
  * Yarn audit outputs one JSON object per line (newline-delimited JSON)
383
426
  */
384
427
  private parseYarnAuditOutput;
428
+ /**
429
+ * Fetch health endpoints assigned to this project for SDK polling
430
+ */
431
+ fetchHealthEndpoints(): Promise<SdkHealthEndpoint[]>;
432
+ /**
433
+ * Submit health check results to the server
434
+ */
435
+ submitHealthResults(results: SdkHealthResult[]): Promise<{
436
+ processed: number;
437
+ total: number;
438
+ }>;
439
+ /**
440
+ * Perform a single health check on an endpoint
441
+ */
442
+ private performHealthCheck;
443
+ /**
444
+ * Setup automatic health check polling
445
+ */
446
+ private setupHealthCheckPolling;
447
+ /**
448
+ * Fetch health endpoints and schedule individual checks
449
+ */
450
+ private fetchAndScheduleHealthChecks;
451
+ /**
452
+ * Schedule individual health check for an endpoint
453
+ */
454
+ private scheduleHealthCheck;
455
+ /**
456
+ * Run a health check and queue the result
457
+ */
458
+ private runHealthCheck;
459
+ /**
460
+ * Flush queued health results to the server
461
+ */
462
+ private flushHealthResults;
463
+ /**
464
+ * Stop all health check timers
465
+ */
466
+ private stopHealthCheckTimers;
385
467
  }
386
468
 
387
- export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
469
+ export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.js CHANGED
@@ -67,8 +67,14 @@ var CONFIG_LIMITS = {
67
67
  // 60 seconds max for all dependency syncs
68
68
  AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
69
69
  // 3 minutes max for all audit paths
70
- REGISTRY_CONCURRENCY_LIMIT: 5
70
+ REGISTRY_CONCURRENCY_LIMIT: 5,
71
71
  // Limit parallel requests to avoid rate limiting
72
+ HEALTH_CHECK_FETCH_INTERVAL_MS: 6e4,
73
+ // 60 seconds (default)
74
+ HEALTH_CHECK_MIN_INTERVAL_MS: 1e4,
75
+ // 10 seconds (min endpoint interval)
76
+ HEALTH_CHECK_MAX_BATCH_SIZE: 50
77
+ // Max results per batch
72
78
  };
73
79
  var MonitorClient = class {
74
80
  constructor(config) {
@@ -82,6 +88,9 @@ var MonitorClient = class {
82
88
  this.lastScanTime = null;
83
89
  this.lastKnownScanRequestedAt = null;
84
90
  this.lastKnownTechScanRequestedAt = null;
91
+ this.healthCheckFetchTimer = null;
92
+ this.healthCheckTimers = /* @__PURE__ */ new Map();
93
+ this.healthCheckResultsQueue = [];
85
94
  if (!config.apiKey || config.apiKey.trim().length === 0) {
86
95
  throw new Error("[MonitorClient] API key is required");
87
96
  }
@@ -96,7 +105,8 @@ var MonitorClient = class {
96
105
  if (!["https:", "http:"].includes(url.protocol)) {
97
106
  throw new Error("[MonitorClient] Endpoint must use HTTP or HTTPS protocol");
98
107
  }
99
- if (url.protocol === "http:" && config.environment !== "development" && config.environment !== "test") {
108
+ const devEnvironments = ["development", "dev", "test", "local"];
109
+ if (url.protocol === "http:" && !devEnvironments.includes(config.environment || "")) {
100
110
  if (!config.allowInsecureHttp) {
101
111
  throw new Error("[MonitorClient] HTTP endpoints require allowInsecureHttp: true in non-development environments");
102
112
  }
@@ -140,6 +150,8 @@ var MonitorClient = class {
140
150
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
141
151
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
142
152
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
153
+ this.healthCheckEnabled = config.healthCheckEnabled || false;
154
+ this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
143
155
  this.startFlushTimer();
144
156
  if (this.trackDependencies) {
145
157
  this.syncDependencies().catch((err) => {
@@ -151,6 +163,11 @@ var MonitorClient = class {
151
163
  console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
152
164
  });
153
165
  }
166
+ if (this.healthCheckEnabled) {
167
+ this.setupHealthCheckPolling().catch((err) => {
168
+ console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
169
+ });
170
+ }
154
171
  }
155
172
  /**
156
173
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -299,7 +316,9 @@ var MonitorClient = class {
299
316
  this.isClosed = true;
300
317
  this.stopFlushTimer();
301
318
  this.stopAuditIntervalTimer();
319
+ this.stopHealthCheckTimers();
302
320
  await this.flush();
321
+ await this.flushHealthResults();
303
322
  }
304
323
  stopAuditIntervalTimer() {
305
324
  if (this.auditIntervalTimer) {
@@ -1189,6 +1208,195 @@ var MonitorClient = class {
1189
1208
  }
1190
1209
  return vulnerabilities;
1191
1210
  }
1211
+ /**
1212
+ * Fetch health endpoints assigned to this project for SDK polling
1213
+ */
1214
+ async fetchHealthEndpoints() {
1215
+ try {
1216
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/endpoints`, {
1217
+ method: "GET",
1218
+ headers: {
1219
+ "Authorization": `Bearer ${this.apiKey}`,
1220
+ "Content-Type": "application/json"
1221
+ }
1222
+ });
1223
+ if (!response.ok) {
1224
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", response.status);
1225
+ return [];
1226
+ }
1227
+ const result = await response.json();
1228
+ return result.data?.endpoints || [];
1229
+ } catch (err) {
1230
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", err instanceof Error ? err.message : String(err));
1231
+ return [];
1232
+ }
1233
+ }
1234
+ /**
1235
+ * Submit health check results to the server
1236
+ */
1237
+ async submitHealthResults(results) {
1238
+ if (results.length === 0) {
1239
+ return { processed: 0, total: 0 };
1240
+ }
1241
+ try {
1242
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/results`, {
1243
+ method: "POST",
1244
+ headers: {
1245
+ "Content-Type": "application/json",
1246
+ "Authorization": `Bearer ${this.apiKey}`
1247
+ },
1248
+ body: JSON.stringify({ results })
1249
+ });
1250
+ if (!response.ok) {
1251
+ const errorText = await response.text().catch(() => "");
1252
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1253
+ }
1254
+ const result = await response.json();
1255
+ return result.data;
1256
+ } catch (err) {
1257
+ console.error("[MonitorClient] Failed to submit health results:", err instanceof Error ? err.message : String(err));
1258
+ throw err;
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Perform a single health check on an endpoint
1263
+ */
1264
+ async performHealthCheck(endpoint) {
1265
+ const startTime = Date.now();
1266
+ try {
1267
+ const requestOptions = {
1268
+ method: endpoint.method,
1269
+ headers: endpoint.headers || {}
1270
+ };
1271
+ if (endpoint.method === "POST" && endpoint.body) {
1272
+ requestOptions.body = JSON.stringify(endpoint.body);
1273
+ requestOptions.headers = {
1274
+ ...requestOptions.headers,
1275
+ "Content-Type": "application/json"
1276
+ };
1277
+ }
1278
+ const response = await this.fetchWithTimeout(
1279
+ endpoint.url,
1280
+ requestOptions,
1281
+ endpoint.timeoutMs
1282
+ );
1283
+ const responseTimeMs = Date.now() - startTime;
1284
+ const statusCode = response.status;
1285
+ let status;
1286
+ if (statusCode === endpoint.expectedStatus) {
1287
+ const slowThreshold = endpoint.timeoutMs * 0.8;
1288
+ if (responseTimeMs > slowThreshold) {
1289
+ status = "DEGRADED";
1290
+ } else {
1291
+ status = "HEALTHY";
1292
+ }
1293
+ } else {
1294
+ status = "UNHEALTHY";
1295
+ }
1296
+ return {
1297
+ endpointId: endpoint.id,
1298
+ status,
1299
+ statusCode,
1300
+ responseTimeMs
1301
+ };
1302
+ } catch (err) {
1303
+ const responseTimeMs = Date.now() - startTime;
1304
+ const errorMessage = err instanceof Error ? err.message : String(err);
1305
+ return {
1306
+ endpointId: endpoint.id,
1307
+ status: "UNHEALTHY",
1308
+ responseTimeMs,
1309
+ errorMessage: this.sanitizeErrorResponse(errorMessage)
1310
+ };
1311
+ }
1312
+ }
1313
+ /**
1314
+ * Setup automatic health check polling
1315
+ */
1316
+ async setupHealthCheckPolling() {
1317
+ console.log("[MonitorClient] Setting up health check polling...");
1318
+ await this.fetchAndScheduleHealthChecks();
1319
+ this.healthCheckFetchTimer = setInterval(() => {
1320
+ this.fetchAndScheduleHealthChecks().catch((err) => {
1321
+ console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1322
+ });
1323
+ }, this.healthCheckFetchIntervalMs);
1324
+ }
1325
+ /**
1326
+ * Fetch health endpoints and schedule individual checks
1327
+ */
1328
+ async fetchAndScheduleHealthChecks() {
1329
+ try {
1330
+ const endpoints = await this.fetchHealthEndpoints();
1331
+ if (endpoints.length === 0) {
1332
+ console.log("[MonitorClient] No health endpoints assigned for SDK polling");
1333
+ this.stopHealthCheckTimers();
1334
+ return;
1335
+ }
1336
+ console.log(`[MonitorClient] Fetched ${endpoints.length} health endpoints for polling`);
1337
+ this.stopHealthCheckTimers();
1338
+ for (const endpoint of endpoints) {
1339
+ this.scheduleHealthCheck(endpoint);
1340
+ }
1341
+ } catch (err) {
1342
+ console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1343
+ }
1344
+ }
1345
+ /**
1346
+ * Schedule individual health check for an endpoint
1347
+ */
1348
+ scheduleHealthCheck(endpoint) {
1349
+ const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1350
+ this.runHealthCheck(endpoint).catch((err) => {
1351
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1352
+ });
1353
+ const timer = setInterval(() => {
1354
+ this.runHealthCheck(endpoint).catch((err) => {
1355
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1356
+ });
1357
+ }, intervalMs);
1358
+ this.healthCheckTimers.set(endpoint.id, timer);
1359
+ }
1360
+ /**
1361
+ * Run a health check and queue the result
1362
+ */
1363
+ async runHealthCheck(endpoint) {
1364
+ const result = await this.performHealthCheck(endpoint);
1365
+ this.healthCheckResultsQueue.push(result);
1366
+ if (this.healthCheckResultsQueue.length >= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1367
+ await this.flushHealthResults();
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Flush queued health results to the server
1372
+ */
1373
+ async flushHealthResults() {
1374
+ if (this.healthCheckResultsQueue.length === 0) return;
1375
+ const results = [...this.healthCheckResultsQueue];
1376
+ this.healthCheckResultsQueue = [];
1377
+ try {
1378
+ const response = await this.submitHealthResults(results);
1379
+ console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1380
+ } catch (err) {
1381
+ console.error("[MonitorClient] Failed to flush health results:", err);
1382
+ if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1383
+ this.healthCheckResultsQueue.unshift(...results);
1384
+ }
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Stop all health check timers
1389
+ */
1390
+ stopHealthCheckTimers() {
1391
+ if (this.healthCheckFetchTimer) {
1392
+ clearInterval(this.healthCheckFetchTimer);
1393
+ this.healthCheckFetchTimer = null;
1394
+ }
1395
+ for (const [endpointId, timer] of this.healthCheckTimers.entries()) {
1396
+ clearInterval(timer);
1397
+ this.healthCheckTimers.delete(endpointId);
1398
+ }
1399
+ }
1192
1400
  };
1193
1401
  // Annotate the CommonJS export names for ESM import in node:
1194
1402
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -31,8 +31,14 @@ var CONFIG_LIMITS = {
31
31
  // 60 seconds max for all dependency syncs
32
32
  AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
33
33
  // 3 minutes max for all audit paths
34
- REGISTRY_CONCURRENCY_LIMIT: 5
34
+ REGISTRY_CONCURRENCY_LIMIT: 5,
35
35
  // Limit parallel requests to avoid rate limiting
36
+ HEALTH_CHECK_FETCH_INTERVAL_MS: 6e4,
37
+ // 60 seconds (default)
38
+ HEALTH_CHECK_MIN_INTERVAL_MS: 1e4,
39
+ // 10 seconds (min endpoint interval)
40
+ HEALTH_CHECK_MAX_BATCH_SIZE: 50
41
+ // Max results per batch
36
42
  };
37
43
  var MonitorClient = class {
38
44
  constructor(config) {
@@ -46,6 +52,9 @@ var MonitorClient = class {
46
52
  this.lastScanTime = null;
47
53
  this.lastKnownScanRequestedAt = null;
48
54
  this.lastKnownTechScanRequestedAt = null;
55
+ this.healthCheckFetchTimer = null;
56
+ this.healthCheckTimers = /* @__PURE__ */ new Map();
57
+ this.healthCheckResultsQueue = [];
49
58
  if (!config.apiKey || config.apiKey.trim().length === 0) {
50
59
  throw new Error("[MonitorClient] API key is required");
51
60
  }
@@ -60,7 +69,8 @@ var MonitorClient = class {
60
69
  if (!["https:", "http:"].includes(url.protocol)) {
61
70
  throw new Error("[MonitorClient] Endpoint must use HTTP or HTTPS protocol");
62
71
  }
63
- if (url.protocol === "http:" && config.environment !== "development" && config.environment !== "test") {
72
+ const devEnvironments = ["development", "dev", "test", "local"];
73
+ if (url.protocol === "http:" && !devEnvironments.includes(config.environment || "")) {
64
74
  if (!config.allowInsecureHttp) {
65
75
  throw new Error("[MonitorClient] HTTP endpoints require allowInsecureHttp: true in non-development environments");
66
76
  }
@@ -104,6 +114,8 @@ var MonitorClient = class {
104
114
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
105
115
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
106
116
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
117
+ this.healthCheckEnabled = config.healthCheckEnabled || false;
118
+ this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
107
119
  this.startFlushTimer();
108
120
  if (this.trackDependencies) {
109
121
  this.syncDependencies().catch((err) => {
@@ -115,6 +127,11 @@ var MonitorClient = class {
115
127
  console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
116
128
  });
117
129
  }
130
+ if (this.healthCheckEnabled) {
131
+ this.setupHealthCheckPolling().catch((err) => {
132
+ console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
133
+ });
134
+ }
118
135
  }
119
136
  /**
120
137
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -263,7 +280,9 @@ var MonitorClient = class {
263
280
  this.isClosed = true;
264
281
  this.stopFlushTimer();
265
282
  this.stopAuditIntervalTimer();
283
+ this.stopHealthCheckTimers();
266
284
  await this.flush();
285
+ await this.flushHealthResults();
267
286
  }
268
287
  stopAuditIntervalTimer() {
269
288
  if (this.auditIntervalTimer) {
@@ -1153,6 +1172,195 @@ var MonitorClient = class {
1153
1172
  }
1154
1173
  return vulnerabilities;
1155
1174
  }
1175
+ /**
1176
+ * Fetch health endpoints assigned to this project for SDK polling
1177
+ */
1178
+ async fetchHealthEndpoints() {
1179
+ try {
1180
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/endpoints`, {
1181
+ method: "GET",
1182
+ headers: {
1183
+ "Authorization": `Bearer ${this.apiKey}`,
1184
+ "Content-Type": "application/json"
1185
+ }
1186
+ });
1187
+ if (!response.ok) {
1188
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", response.status);
1189
+ return [];
1190
+ }
1191
+ const result = await response.json();
1192
+ return result.data?.endpoints || [];
1193
+ } catch (err) {
1194
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", err instanceof Error ? err.message : String(err));
1195
+ return [];
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Submit health check results to the server
1200
+ */
1201
+ async submitHealthResults(results) {
1202
+ if (results.length === 0) {
1203
+ return { processed: 0, total: 0 };
1204
+ }
1205
+ try {
1206
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/results`, {
1207
+ method: "POST",
1208
+ headers: {
1209
+ "Content-Type": "application/json",
1210
+ "Authorization": `Bearer ${this.apiKey}`
1211
+ },
1212
+ body: JSON.stringify({ results })
1213
+ });
1214
+ if (!response.ok) {
1215
+ const errorText = await response.text().catch(() => "");
1216
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1217
+ }
1218
+ const result = await response.json();
1219
+ return result.data;
1220
+ } catch (err) {
1221
+ console.error("[MonitorClient] Failed to submit health results:", err instanceof Error ? err.message : String(err));
1222
+ throw err;
1223
+ }
1224
+ }
1225
+ /**
1226
+ * Perform a single health check on an endpoint
1227
+ */
1228
+ async performHealthCheck(endpoint) {
1229
+ const startTime = Date.now();
1230
+ try {
1231
+ const requestOptions = {
1232
+ method: endpoint.method,
1233
+ headers: endpoint.headers || {}
1234
+ };
1235
+ if (endpoint.method === "POST" && endpoint.body) {
1236
+ requestOptions.body = JSON.stringify(endpoint.body);
1237
+ requestOptions.headers = {
1238
+ ...requestOptions.headers,
1239
+ "Content-Type": "application/json"
1240
+ };
1241
+ }
1242
+ const response = await this.fetchWithTimeout(
1243
+ endpoint.url,
1244
+ requestOptions,
1245
+ endpoint.timeoutMs
1246
+ );
1247
+ const responseTimeMs = Date.now() - startTime;
1248
+ const statusCode = response.status;
1249
+ let status;
1250
+ if (statusCode === endpoint.expectedStatus) {
1251
+ const slowThreshold = endpoint.timeoutMs * 0.8;
1252
+ if (responseTimeMs > slowThreshold) {
1253
+ status = "DEGRADED";
1254
+ } else {
1255
+ status = "HEALTHY";
1256
+ }
1257
+ } else {
1258
+ status = "UNHEALTHY";
1259
+ }
1260
+ return {
1261
+ endpointId: endpoint.id,
1262
+ status,
1263
+ statusCode,
1264
+ responseTimeMs
1265
+ };
1266
+ } catch (err) {
1267
+ const responseTimeMs = Date.now() - startTime;
1268
+ const errorMessage = err instanceof Error ? err.message : String(err);
1269
+ return {
1270
+ endpointId: endpoint.id,
1271
+ status: "UNHEALTHY",
1272
+ responseTimeMs,
1273
+ errorMessage: this.sanitizeErrorResponse(errorMessage)
1274
+ };
1275
+ }
1276
+ }
1277
+ /**
1278
+ * Setup automatic health check polling
1279
+ */
1280
+ async setupHealthCheckPolling() {
1281
+ console.log("[MonitorClient] Setting up health check polling...");
1282
+ await this.fetchAndScheduleHealthChecks();
1283
+ this.healthCheckFetchTimer = setInterval(() => {
1284
+ this.fetchAndScheduleHealthChecks().catch((err) => {
1285
+ console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1286
+ });
1287
+ }, this.healthCheckFetchIntervalMs);
1288
+ }
1289
+ /**
1290
+ * Fetch health endpoints and schedule individual checks
1291
+ */
1292
+ async fetchAndScheduleHealthChecks() {
1293
+ try {
1294
+ const endpoints = await this.fetchHealthEndpoints();
1295
+ if (endpoints.length === 0) {
1296
+ console.log("[MonitorClient] No health endpoints assigned for SDK polling");
1297
+ this.stopHealthCheckTimers();
1298
+ return;
1299
+ }
1300
+ console.log(`[MonitorClient] Fetched ${endpoints.length} health endpoints for polling`);
1301
+ this.stopHealthCheckTimers();
1302
+ for (const endpoint of endpoints) {
1303
+ this.scheduleHealthCheck(endpoint);
1304
+ }
1305
+ } catch (err) {
1306
+ console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Schedule individual health check for an endpoint
1311
+ */
1312
+ scheduleHealthCheck(endpoint) {
1313
+ const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1314
+ this.runHealthCheck(endpoint).catch((err) => {
1315
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1316
+ });
1317
+ const timer = setInterval(() => {
1318
+ this.runHealthCheck(endpoint).catch((err) => {
1319
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1320
+ });
1321
+ }, intervalMs);
1322
+ this.healthCheckTimers.set(endpoint.id, timer);
1323
+ }
1324
+ /**
1325
+ * Run a health check and queue the result
1326
+ */
1327
+ async runHealthCheck(endpoint) {
1328
+ const result = await this.performHealthCheck(endpoint);
1329
+ this.healthCheckResultsQueue.push(result);
1330
+ if (this.healthCheckResultsQueue.length >= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1331
+ await this.flushHealthResults();
1332
+ }
1333
+ }
1334
+ /**
1335
+ * Flush queued health results to the server
1336
+ */
1337
+ async flushHealthResults() {
1338
+ if (this.healthCheckResultsQueue.length === 0) return;
1339
+ const results = [...this.healthCheckResultsQueue];
1340
+ this.healthCheckResultsQueue = [];
1341
+ try {
1342
+ const response = await this.submitHealthResults(results);
1343
+ console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1344
+ } catch (err) {
1345
+ console.error("[MonitorClient] Failed to flush health results:", err);
1346
+ if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1347
+ this.healthCheckResultsQueue.unshift(...results);
1348
+ }
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Stop all health check timers
1353
+ */
1354
+ stopHealthCheckTimers() {
1355
+ if (this.healthCheckFetchTimer) {
1356
+ clearInterval(this.healthCheckFetchTimer);
1357
+ this.healthCheckFetchTimer = null;
1358
+ }
1359
+ for (const [endpointId, timer] of this.healthCheckTimers.entries()) {
1360
+ clearInterval(timer);
1361
+ this.healthCheckTimers.delete(endpointId);
1362
+ }
1363
+ }
1156
1364
  };
1157
1365
  export {
1158
1366
  MonitorClient
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceon-oy/monitor-sdk",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Client SDK for Ceon Monitor - Error tracking, health monitoring, security events, and vulnerability scanning",
5
5
  "author": "Ceon",
6
6
  "license": "MIT",