@ceon-oy/monitor-sdk 1.1.5 → 1.2.1

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,12 @@ 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;
228
+ private healthCheckFlushTimer;
185
229
  constructor(config: MonitorClientConfig);
186
230
  /**
187
231
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -382,6 +426,45 @@ declare class MonitorClient {
382
426
  * Yarn audit outputs one JSON object per line (newline-delimited JSON)
383
427
  */
384
428
  private parseYarnAuditOutput;
429
+ /**
430
+ * Fetch health endpoints assigned to this project for SDK polling
431
+ */
432
+ fetchHealthEndpoints(): Promise<SdkHealthEndpoint[]>;
433
+ /**
434
+ * Submit health check results to the server
435
+ */
436
+ submitHealthResults(results: SdkHealthResult[]): Promise<{
437
+ processed: number;
438
+ total: number;
439
+ }>;
440
+ /**
441
+ * Perform a single health check on an endpoint
442
+ */
443
+ private performHealthCheck;
444
+ /**
445
+ * Setup automatic health check polling
446
+ */
447
+ private setupHealthCheckPolling;
448
+ /**
449
+ * Fetch health endpoints and schedule individual checks
450
+ */
451
+ private fetchAndScheduleHealthChecks;
452
+ /**
453
+ * Schedule individual health check for an endpoint
454
+ */
455
+ private scheduleHealthCheck;
456
+ /**
457
+ * Run a health check and queue the result
458
+ */
459
+ private runHealthCheck;
460
+ /**
461
+ * Flush queued health results to the server
462
+ */
463
+ private flushHealthResults;
464
+ /**
465
+ * Stop all health check timers
466
+ */
467
+ private stopHealthCheckTimers;
385
468
  }
386
469
 
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 };
470
+ 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,12 @@ 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;
228
+ private healthCheckFlushTimer;
185
229
  constructor(config: MonitorClientConfig);
186
230
  /**
187
231
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -382,6 +426,45 @@ declare class MonitorClient {
382
426
  * Yarn audit outputs one JSON object per line (newline-delimited JSON)
383
427
  */
384
428
  private parseYarnAuditOutput;
429
+ /**
430
+ * Fetch health endpoints assigned to this project for SDK polling
431
+ */
432
+ fetchHealthEndpoints(): Promise<SdkHealthEndpoint[]>;
433
+ /**
434
+ * Submit health check results to the server
435
+ */
436
+ submitHealthResults(results: SdkHealthResult[]): Promise<{
437
+ processed: number;
438
+ total: number;
439
+ }>;
440
+ /**
441
+ * Perform a single health check on an endpoint
442
+ */
443
+ private performHealthCheck;
444
+ /**
445
+ * Setup automatic health check polling
446
+ */
447
+ private setupHealthCheckPolling;
448
+ /**
449
+ * Fetch health endpoints and schedule individual checks
450
+ */
451
+ private fetchAndScheduleHealthChecks;
452
+ /**
453
+ * Schedule individual health check for an endpoint
454
+ */
455
+ private scheduleHealthCheck;
456
+ /**
457
+ * Run a health check and queue the result
458
+ */
459
+ private runHealthCheck;
460
+ /**
461
+ * Flush queued health results to the server
462
+ */
463
+ private flushHealthResults;
464
+ /**
465
+ * Stop all health check timers
466
+ */
467
+ private stopHealthCheckTimers;
385
468
  }
386
469
 
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 };
470
+ 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,16 @@ 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
78
+ HEALTH_CHECK_FLUSH_INTERVAL_MS: 3e4
79
+ // Flush health results every 30 seconds
72
80
  };
73
81
  var MonitorClient = class {
74
82
  constructor(config) {
@@ -82,6 +90,10 @@ var MonitorClient = class {
82
90
  this.lastScanTime = null;
83
91
  this.lastKnownScanRequestedAt = null;
84
92
  this.lastKnownTechScanRequestedAt = null;
93
+ this.healthCheckFetchTimer = null;
94
+ this.healthCheckTimers = /* @__PURE__ */ new Map();
95
+ this.healthCheckResultsQueue = [];
96
+ this.healthCheckFlushTimer = null;
85
97
  if (!config.apiKey || config.apiKey.trim().length === 0) {
86
98
  throw new Error("[MonitorClient] API key is required");
87
99
  }
@@ -141,6 +153,8 @@ var MonitorClient = class {
141
153
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
142
154
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
143
155
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
156
+ this.healthCheckEnabled = config.healthCheckEnabled || false;
157
+ this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
144
158
  this.startFlushTimer();
145
159
  if (this.trackDependencies) {
146
160
  this.syncDependencies().catch((err) => {
@@ -152,6 +166,11 @@ var MonitorClient = class {
152
166
  console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
153
167
  });
154
168
  }
169
+ if (this.healthCheckEnabled) {
170
+ this.setupHealthCheckPolling().catch((err) => {
171
+ console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
172
+ });
173
+ }
155
174
  }
156
175
  /**
157
176
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -300,7 +319,9 @@ var MonitorClient = class {
300
319
  this.isClosed = true;
301
320
  this.stopFlushTimer();
302
321
  this.stopAuditIntervalTimer();
322
+ this.stopHealthCheckTimers();
303
323
  await this.flush();
324
+ await this.flushHealthResults();
304
325
  }
305
326
  stopAuditIntervalTimer() {
306
327
  if (this.auditIntervalTimer) {
@@ -1190,6 +1211,204 @@ var MonitorClient = class {
1190
1211
  }
1191
1212
  return vulnerabilities;
1192
1213
  }
1214
+ /**
1215
+ * Fetch health endpoints assigned to this project for SDK polling
1216
+ */
1217
+ async fetchHealthEndpoints() {
1218
+ try {
1219
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/endpoints`, {
1220
+ method: "GET",
1221
+ headers: {
1222
+ "Authorization": `Bearer ${this.apiKey}`,
1223
+ "Content-Type": "application/json"
1224
+ }
1225
+ });
1226
+ if (!response.ok) {
1227
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", response.status);
1228
+ return [];
1229
+ }
1230
+ const result = await response.json();
1231
+ return result.data?.endpoints || [];
1232
+ } catch (err) {
1233
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", err instanceof Error ? err.message : String(err));
1234
+ return [];
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Submit health check results to the server
1239
+ */
1240
+ async submitHealthResults(results) {
1241
+ if (results.length === 0) {
1242
+ return { processed: 0, total: 0 };
1243
+ }
1244
+ try {
1245
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/results`, {
1246
+ method: "POST",
1247
+ headers: {
1248
+ "Content-Type": "application/json",
1249
+ "Authorization": `Bearer ${this.apiKey}`
1250
+ },
1251
+ body: JSON.stringify({ results })
1252
+ });
1253
+ if (!response.ok) {
1254
+ const errorText = await response.text().catch(() => "");
1255
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1256
+ }
1257
+ const result = await response.json();
1258
+ return result.data;
1259
+ } catch (err) {
1260
+ console.error("[MonitorClient] Failed to submit health results:", err instanceof Error ? err.message : String(err));
1261
+ throw err;
1262
+ }
1263
+ }
1264
+ /**
1265
+ * Perform a single health check on an endpoint
1266
+ */
1267
+ async performHealthCheck(endpoint) {
1268
+ const startTime = Date.now();
1269
+ try {
1270
+ const requestOptions = {
1271
+ method: endpoint.method,
1272
+ headers: endpoint.headers || {}
1273
+ };
1274
+ if (endpoint.method === "POST" && endpoint.body) {
1275
+ requestOptions.body = JSON.stringify(endpoint.body);
1276
+ requestOptions.headers = {
1277
+ ...requestOptions.headers,
1278
+ "Content-Type": "application/json"
1279
+ };
1280
+ }
1281
+ const response = await this.fetchWithTimeout(
1282
+ endpoint.url,
1283
+ requestOptions,
1284
+ endpoint.timeoutMs
1285
+ );
1286
+ const responseTimeMs = Date.now() - startTime;
1287
+ const statusCode = response.status;
1288
+ let status;
1289
+ if (statusCode === endpoint.expectedStatus) {
1290
+ const slowThreshold = endpoint.timeoutMs * 0.8;
1291
+ if (responseTimeMs > slowThreshold) {
1292
+ status = "DEGRADED";
1293
+ } else {
1294
+ status = "HEALTHY";
1295
+ }
1296
+ } else {
1297
+ status = "UNHEALTHY";
1298
+ }
1299
+ return {
1300
+ endpointId: endpoint.id,
1301
+ status,
1302
+ statusCode,
1303
+ responseTimeMs
1304
+ };
1305
+ } catch (err) {
1306
+ const responseTimeMs = Date.now() - startTime;
1307
+ const errorMessage = err instanceof Error ? err.message : String(err);
1308
+ return {
1309
+ endpointId: endpoint.id,
1310
+ status: "UNHEALTHY",
1311
+ responseTimeMs,
1312
+ errorMessage: this.sanitizeErrorResponse(errorMessage)
1313
+ };
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Setup automatic health check polling
1318
+ */
1319
+ async setupHealthCheckPolling() {
1320
+ console.log("[MonitorClient] Setting up health check polling...");
1321
+ await this.fetchAndScheduleHealthChecks();
1322
+ this.healthCheckFetchTimer = setInterval(() => {
1323
+ this.fetchAndScheduleHealthChecks().catch((err) => {
1324
+ console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1325
+ });
1326
+ }, this.healthCheckFetchIntervalMs);
1327
+ this.healthCheckFlushTimer = setInterval(() => {
1328
+ this.flushHealthResults().catch((err) => {
1329
+ console.error("[MonitorClient] Failed to flush health results:", err);
1330
+ });
1331
+ }, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
1332
+ }
1333
+ /**
1334
+ * Fetch health endpoints and schedule individual checks
1335
+ */
1336
+ async fetchAndScheduleHealthChecks() {
1337
+ try {
1338
+ const endpoints = await this.fetchHealthEndpoints();
1339
+ if (endpoints.length === 0) {
1340
+ console.log("[MonitorClient] No health endpoints assigned for SDK polling");
1341
+ this.stopHealthCheckTimers();
1342
+ return;
1343
+ }
1344
+ console.log(`[MonitorClient] Fetched ${endpoints.length} health endpoints for polling`);
1345
+ this.stopHealthCheckTimers();
1346
+ for (const endpoint of endpoints) {
1347
+ this.scheduleHealthCheck(endpoint);
1348
+ }
1349
+ } catch (err) {
1350
+ console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1351
+ }
1352
+ }
1353
+ /**
1354
+ * Schedule individual health check for an endpoint
1355
+ */
1356
+ scheduleHealthCheck(endpoint) {
1357
+ const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1358
+ this.runHealthCheck(endpoint).catch((err) => {
1359
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1360
+ });
1361
+ const timer = setInterval(() => {
1362
+ this.runHealthCheck(endpoint).catch((err) => {
1363
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1364
+ });
1365
+ }, intervalMs);
1366
+ this.healthCheckTimers.set(endpoint.id, timer);
1367
+ }
1368
+ /**
1369
+ * Run a health check and queue the result
1370
+ */
1371
+ async runHealthCheck(endpoint) {
1372
+ const result = await this.performHealthCheck(endpoint);
1373
+ this.healthCheckResultsQueue.push(result);
1374
+ if (this.healthCheckResultsQueue.length >= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1375
+ await this.flushHealthResults();
1376
+ }
1377
+ }
1378
+ /**
1379
+ * Flush queued health results to the server
1380
+ */
1381
+ async flushHealthResults() {
1382
+ if (this.healthCheckResultsQueue.length === 0) return;
1383
+ const results = [...this.healthCheckResultsQueue];
1384
+ this.healthCheckResultsQueue = [];
1385
+ try {
1386
+ const response = await this.submitHealthResults(results);
1387
+ console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1388
+ } catch (err) {
1389
+ console.error("[MonitorClient] Failed to flush health results:", err);
1390
+ if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1391
+ this.healthCheckResultsQueue.unshift(...results);
1392
+ }
1393
+ }
1394
+ }
1395
+ /**
1396
+ * Stop all health check timers
1397
+ */
1398
+ stopHealthCheckTimers() {
1399
+ if (this.healthCheckFetchTimer) {
1400
+ clearInterval(this.healthCheckFetchTimer);
1401
+ this.healthCheckFetchTimer = null;
1402
+ }
1403
+ if (this.healthCheckFlushTimer) {
1404
+ clearInterval(this.healthCheckFlushTimer);
1405
+ this.healthCheckFlushTimer = null;
1406
+ }
1407
+ for (const [endpointId, timer] of this.healthCheckTimers.entries()) {
1408
+ clearInterval(timer);
1409
+ this.healthCheckTimers.delete(endpointId);
1410
+ }
1411
+ }
1193
1412
  };
1194
1413
  // Annotate the CommonJS export names for ESM import in node:
1195
1414
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -31,8 +31,16 @@ 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
42
+ HEALTH_CHECK_FLUSH_INTERVAL_MS: 3e4
43
+ // Flush health results every 30 seconds
36
44
  };
37
45
  var MonitorClient = class {
38
46
  constructor(config) {
@@ -46,6 +54,10 @@ var MonitorClient = class {
46
54
  this.lastScanTime = null;
47
55
  this.lastKnownScanRequestedAt = null;
48
56
  this.lastKnownTechScanRequestedAt = null;
57
+ this.healthCheckFetchTimer = null;
58
+ this.healthCheckTimers = /* @__PURE__ */ new Map();
59
+ this.healthCheckResultsQueue = [];
60
+ this.healthCheckFlushTimer = null;
49
61
  if (!config.apiKey || config.apiKey.trim().length === 0) {
50
62
  throw new Error("[MonitorClient] API key is required");
51
63
  }
@@ -105,6 +117,8 @@ var MonitorClient = class {
105
117
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
106
118
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
107
119
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
120
+ this.healthCheckEnabled = config.healthCheckEnabled || false;
121
+ this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
108
122
  this.startFlushTimer();
109
123
  if (this.trackDependencies) {
110
124
  this.syncDependencies().catch((err) => {
@@ -116,6 +130,11 @@ var MonitorClient = class {
116
130
  console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
117
131
  });
118
132
  }
133
+ if (this.healthCheckEnabled) {
134
+ this.setupHealthCheckPolling().catch((err) => {
135
+ console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
136
+ });
137
+ }
119
138
  }
120
139
  /**
121
140
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -264,7 +283,9 @@ var MonitorClient = class {
264
283
  this.isClosed = true;
265
284
  this.stopFlushTimer();
266
285
  this.stopAuditIntervalTimer();
286
+ this.stopHealthCheckTimers();
267
287
  await this.flush();
288
+ await this.flushHealthResults();
268
289
  }
269
290
  stopAuditIntervalTimer() {
270
291
  if (this.auditIntervalTimer) {
@@ -1154,6 +1175,204 @@ var MonitorClient = class {
1154
1175
  }
1155
1176
  return vulnerabilities;
1156
1177
  }
1178
+ /**
1179
+ * Fetch health endpoints assigned to this project for SDK polling
1180
+ */
1181
+ async fetchHealthEndpoints() {
1182
+ try {
1183
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/endpoints`, {
1184
+ method: "GET",
1185
+ headers: {
1186
+ "Authorization": `Bearer ${this.apiKey}`,
1187
+ "Content-Type": "application/json"
1188
+ }
1189
+ });
1190
+ if (!response.ok) {
1191
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", response.status);
1192
+ return [];
1193
+ }
1194
+ const result = await response.json();
1195
+ return result.data?.endpoints || [];
1196
+ } catch (err) {
1197
+ console.warn("[MonitorClient] Failed to fetch health endpoints:", err instanceof Error ? err.message : String(err));
1198
+ return [];
1199
+ }
1200
+ }
1201
+ /**
1202
+ * Submit health check results to the server
1203
+ */
1204
+ async submitHealthResults(results) {
1205
+ if (results.length === 0) {
1206
+ return { processed: 0, total: 0 };
1207
+ }
1208
+ try {
1209
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/health/sdk/results`, {
1210
+ method: "POST",
1211
+ headers: {
1212
+ "Content-Type": "application/json",
1213
+ "Authorization": `Bearer ${this.apiKey}`
1214
+ },
1215
+ body: JSON.stringify({ results })
1216
+ });
1217
+ if (!response.ok) {
1218
+ const errorText = await response.text().catch(() => "");
1219
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1220
+ }
1221
+ const result = await response.json();
1222
+ return result.data;
1223
+ } catch (err) {
1224
+ console.error("[MonitorClient] Failed to submit health results:", err instanceof Error ? err.message : String(err));
1225
+ throw err;
1226
+ }
1227
+ }
1228
+ /**
1229
+ * Perform a single health check on an endpoint
1230
+ */
1231
+ async performHealthCheck(endpoint) {
1232
+ const startTime = Date.now();
1233
+ try {
1234
+ const requestOptions = {
1235
+ method: endpoint.method,
1236
+ headers: endpoint.headers || {}
1237
+ };
1238
+ if (endpoint.method === "POST" && endpoint.body) {
1239
+ requestOptions.body = JSON.stringify(endpoint.body);
1240
+ requestOptions.headers = {
1241
+ ...requestOptions.headers,
1242
+ "Content-Type": "application/json"
1243
+ };
1244
+ }
1245
+ const response = await this.fetchWithTimeout(
1246
+ endpoint.url,
1247
+ requestOptions,
1248
+ endpoint.timeoutMs
1249
+ );
1250
+ const responseTimeMs = Date.now() - startTime;
1251
+ const statusCode = response.status;
1252
+ let status;
1253
+ if (statusCode === endpoint.expectedStatus) {
1254
+ const slowThreshold = endpoint.timeoutMs * 0.8;
1255
+ if (responseTimeMs > slowThreshold) {
1256
+ status = "DEGRADED";
1257
+ } else {
1258
+ status = "HEALTHY";
1259
+ }
1260
+ } else {
1261
+ status = "UNHEALTHY";
1262
+ }
1263
+ return {
1264
+ endpointId: endpoint.id,
1265
+ status,
1266
+ statusCode,
1267
+ responseTimeMs
1268
+ };
1269
+ } catch (err) {
1270
+ const responseTimeMs = Date.now() - startTime;
1271
+ const errorMessage = err instanceof Error ? err.message : String(err);
1272
+ return {
1273
+ endpointId: endpoint.id,
1274
+ status: "UNHEALTHY",
1275
+ responseTimeMs,
1276
+ errorMessage: this.sanitizeErrorResponse(errorMessage)
1277
+ };
1278
+ }
1279
+ }
1280
+ /**
1281
+ * Setup automatic health check polling
1282
+ */
1283
+ async setupHealthCheckPolling() {
1284
+ console.log("[MonitorClient] Setting up health check polling...");
1285
+ await this.fetchAndScheduleHealthChecks();
1286
+ this.healthCheckFetchTimer = setInterval(() => {
1287
+ this.fetchAndScheduleHealthChecks().catch((err) => {
1288
+ console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1289
+ });
1290
+ }, this.healthCheckFetchIntervalMs);
1291
+ this.healthCheckFlushTimer = setInterval(() => {
1292
+ this.flushHealthResults().catch((err) => {
1293
+ console.error("[MonitorClient] Failed to flush health results:", err);
1294
+ });
1295
+ }, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
1296
+ }
1297
+ /**
1298
+ * Fetch health endpoints and schedule individual checks
1299
+ */
1300
+ async fetchAndScheduleHealthChecks() {
1301
+ try {
1302
+ const endpoints = await this.fetchHealthEndpoints();
1303
+ if (endpoints.length === 0) {
1304
+ console.log("[MonitorClient] No health endpoints assigned for SDK polling");
1305
+ this.stopHealthCheckTimers();
1306
+ return;
1307
+ }
1308
+ console.log(`[MonitorClient] Fetched ${endpoints.length} health endpoints for polling`);
1309
+ this.stopHealthCheckTimers();
1310
+ for (const endpoint of endpoints) {
1311
+ this.scheduleHealthCheck(endpoint);
1312
+ }
1313
+ } catch (err) {
1314
+ console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1315
+ }
1316
+ }
1317
+ /**
1318
+ * Schedule individual health check for an endpoint
1319
+ */
1320
+ scheduleHealthCheck(endpoint) {
1321
+ const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1322
+ this.runHealthCheck(endpoint).catch((err) => {
1323
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1324
+ });
1325
+ const timer = setInterval(() => {
1326
+ this.runHealthCheck(endpoint).catch((err) => {
1327
+ console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1328
+ });
1329
+ }, intervalMs);
1330
+ this.healthCheckTimers.set(endpoint.id, timer);
1331
+ }
1332
+ /**
1333
+ * Run a health check and queue the result
1334
+ */
1335
+ async runHealthCheck(endpoint) {
1336
+ const result = await this.performHealthCheck(endpoint);
1337
+ this.healthCheckResultsQueue.push(result);
1338
+ if (this.healthCheckResultsQueue.length >= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1339
+ await this.flushHealthResults();
1340
+ }
1341
+ }
1342
+ /**
1343
+ * Flush queued health results to the server
1344
+ */
1345
+ async flushHealthResults() {
1346
+ if (this.healthCheckResultsQueue.length === 0) return;
1347
+ const results = [...this.healthCheckResultsQueue];
1348
+ this.healthCheckResultsQueue = [];
1349
+ try {
1350
+ const response = await this.submitHealthResults(results);
1351
+ console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1352
+ } catch (err) {
1353
+ console.error("[MonitorClient] Failed to flush health results:", err);
1354
+ if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1355
+ this.healthCheckResultsQueue.unshift(...results);
1356
+ }
1357
+ }
1358
+ }
1359
+ /**
1360
+ * Stop all health check timers
1361
+ */
1362
+ stopHealthCheckTimers() {
1363
+ if (this.healthCheckFetchTimer) {
1364
+ clearInterval(this.healthCheckFetchTimer);
1365
+ this.healthCheckFetchTimer = null;
1366
+ }
1367
+ if (this.healthCheckFlushTimer) {
1368
+ clearInterval(this.healthCheckFlushTimer);
1369
+ this.healthCheckFlushTimer = null;
1370
+ }
1371
+ for (const [endpointId, timer] of this.healthCheckTimers.entries()) {
1372
+ clearInterval(timer);
1373
+ this.healthCheckTimers.delete(endpointId);
1374
+ }
1375
+ }
1157
1376
  };
1158
1377
  export {
1159
1378
  MonitorClient
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceon-oy/monitor-sdk",
3
- "version": "1.1.5",
3
+ "version": "1.2.1",
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",