@ceon-oy/monitor-sdk 1.5.1 → 1.5.3
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/dist/index.d.mts +89 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +268 -63
- package/dist/index.mjs +268 -63
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -217,6 +217,7 @@ declare class MonitorClient {
|
|
|
217
217
|
private lastScanTime;
|
|
218
218
|
private lastKnownScanRequestedAt;
|
|
219
219
|
private lastKnownTechScanRequestedAt;
|
|
220
|
+
private currentAuditIntervalHours;
|
|
220
221
|
private versionCheckEnabled;
|
|
221
222
|
private auditTimeoutMs;
|
|
222
223
|
private registryTimeoutMs;
|
|
@@ -260,6 +261,21 @@ declare class MonitorClient {
|
|
|
260
261
|
flush(): Promise<void>;
|
|
261
262
|
private getErrorKey;
|
|
262
263
|
close(): Promise<void>;
|
|
264
|
+
/**
|
|
265
|
+
* Node's fetch() (undici) wraps network-level failures (ECONNREFUSED, DNS lookup
|
|
266
|
+
* failures, TLS errors, ...) in a generic `TypeError: fetch failed`, with the real
|
|
267
|
+
* reason attached via the standard `cause` chain. Without unwrapping it, every
|
|
268
|
+
* network failure across the SDK is reported as the same opaque "fetch failed"
|
|
269
|
+
* message, with no way to diagnose it from the dashboard.
|
|
270
|
+
*/
|
|
271
|
+
private describeErrorCause;
|
|
272
|
+
private truncateDescription;
|
|
273
|
+
/**
|
|
274
|
+
* Run `fn`, swallowing any throw and returning undefined instead. Used to read
|
|
275
|
+
* possibly-hostile Error fields (a throwing message/stack/cause getter) without
|
|
276
|
+
* losing whatever else was already read — each call degrades independently.
|
|
277
|
+
*/
|
|
278
|
+
private safe;
|
|
263
279
|
/**
|
|
264
280
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
265
281
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -269,6 +285,7 @@ declare class MonitorClient {
|
|
|
269
285
|
private startSdkErrorFlushTimer;
|
|
270
286
|
private stopSdkErrorFlushTimer;
|
|
271
287
|
private flushSdkErrors;
|
|
288
|
+
private clearAuditIntervalTimer;
|
|
272
289
|
private stopAuditIntervalTimer;
|
|
273
290
|
/**
|
|
274
291
|
* Fetch project settings from the monitoring server.
|
|
@@ -292,6 +309,12 @@ declare class MonitorClient {
|
|
|
292
309
|
* Also sets up polling for on-demand scan requests from the server.
|
|
293
310
|
*/
|
|
294
311
|
private setupAutoAudit;
|
|
312
|
+
/**
|
|
313
|
+
* (Re)configure the recurring audit timer for the given interval, replacing
|
|
314
|
+
* whichever timer is currently running (if any). intervalHours <= 0 leaves
|
|
315
|
+
* scanning disabled.
|
|
316
|
+
*/
|
|
317
|
+
private setAuditInterval;
|
|
295
318
|
/**
|
|
296
319
|
* Run a vulnerability scan and track the time it was run.
|
|
297
320
|
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
@@ -317,6 +340,55 @@ declare class MonitorClient {
|
|
|
317
340
|
private startFlushTimer;
|
|
318
341
|
private stopFlushTimer;
|
|
319
342
|
syncDependencies(): Promise<void>;
|
|
343
|
+
/**
|
|
344
|
+
* How many sequential registry batches a version-fetch over this many packages
|
|
345
|
+
* will take, given the shared concurrency limit. Shared by fetchLatestVersions
|
|
346
|
+
* (which does the batching) and calculateSyncTimeoutMs (which budgets for it),
|
|
347
|
+
* so the two can't drift out of sync with each other.
|
|
348
|
+
*/
|
|
349
|
+
private registryBatchCount;
|
|
350
|
+
/**
|
|
351
|
+
* Estimate a timeout budget that scales with how many roughly-equal-cost units of
|
|
352
|
+
* sequential work will run (registry batches, audit paths, ...), clamped to a
|
|
353
|
+
* floor/ceiling. Shared by calculateSyncTimeoutMs and calculateMultiPathAuditTimeoutMs
|
|
354
|
+
* so the two scaling formulas can't drift apart. At count=0 this always collapses to
|
|
355
|
+
* floorMs, since overheadMs alone never exceeds a floor sized for real work.
|
|
356
|
+
*/
|
|
357
|
+
private estimateScaledTimeoutMs;
|
|
358
|
+
/**
|
|
359
|
+
* Scale the dependency sync timeout with how many registry lookups AND how many
|
|
360
|
+
* sequential POSTs (one per source, via sendTechnologiesWithEnvironment) it will
|
|
361
|
+
* make, so large dependency lists, many dependencySources, or a large requestTimeoutMs
|
|
362
|
+
* don't spuriously time out against a fixed budget.
|
|
363
|
+
*
|
|
364
|
+
* The POST-phase term (sources.length * requestTimeoutMs) is included even when
|
|
365
|
+
* version checking is off: performDependencySync still does one sequential POST per
|
|
366
|
+
* source regardless, so a flat overhead that ignores source count would under-budget
|
|
367
|
+
* that phase exactly the way a flat registry-batch estimate would under-budget large
|
|
368
|
+
* dependency lists.
|
|
369
|
+
*
|
|
370
|
+
* Registry batches are summed PER SOURCE (not over the combined total) because
|
|
371
|
+
* performDependencySync/fetchLatestVersions batch each source independently and
|
|
372
|
+
* sequentially — ceil(a/n) + ceil(b/n) can be larger than ceil((a+b)/n), so a
|
|
373
|
+
* single batch count over the combined total would under-estimate the real
|
|
374
|
+
* number of sequential registry round-trips when many small sources are configured.
|
|
375
|
+
*/
|
|
376
|
+
private calculateSyncTimeoutMs;
|
|
377
|
+
/**
|
|
378
|
+
* Reads happen before syncDependencies' timeout/AbortSignal exists (see comment
|
|
379
|
+
* there), so this phase bounds itself with a wall-clock deadline on a budget that
|
|
380
|
+
* scales with how many sources are configured (mirroring calculateSyncTimeoutMs's
|
|
381
|
+
* scaling principle for the network phase). A plain deadline timestamp — not an
|
|
382
|
+
* AbortController — is enough here: readPackageJsonFromPath does purely synchronous,
|
|
383
|
+
* non-cancelable fs calls, so there is nothing for a signal to actually interrupt;
|
|
384
|
+
* it can only ever be checked between iterations, exactly like a deadline check.
|
|
385
|
+
* Once the budget is spent, stop reading further sources rather than always paying
|
|
386
|
+
* the full read cost for every configured dependencySource regardless of how long
|
|
387
|
+
* it's already taken — and report it via reportError, not just a console.warn, so a
|
|
388
|
+
* truncated sync isn't silently indistinguishable from a fully successful one on
|
|
389
|
+
* the dashboard.
|
|
390
|
+
*/
|
|
391
|
+
private loadDependencySources;
|
|
320
392
|
private performDependencySync;
|
|
321
393
|
/**
|
|
322
394
|
* Enrich technologies with latest version information from npm registry.
|
|
@@ -416,6 +488,13 @@ declare class MonitorClient {
|
|
|
416
488
|
* Only works in Node.js environment (not browser/bundled).
|
|
417
489
|
*/
|
|
418
490
|
private runCommandWithTimeout;
|
|
491
|
+
/**
|
|
492
|
+
* `npm audit --json` can exit without emitting valid JSON on stdout — e.g. the
|
|
493
|
+
* registry is unreachable, or a fatal npm error is written to stderr instead.
|
|
494
|
+
* Treat that as a soft failure (like a timeout) rather than throwing, so it
|
|
495
|
+
* doesn't surface as a confusing "Unexpected end of JSON input" SDK_ERROR.
|
|
496
|
+
*/
|
|
497
|
+
private parseNpmAuditJson;
|
|
419
498
|
/**
|
|
420
499
|
* Run npm audit and send results to the monitoring server.
|
|
421
500
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -438,6 +517,13 @@ declare class MonitorClient {
|
|
|
438
517
|
* @returns Combined summary of all audit results
|
|
439
518
|
*/
|
|
440
519
|
auditMultiplePaths(): Promise<MultiAuditSummary | null>;
|
|
520
|
+
/**
|
|
521
|
+
* Scale the multi-path audit timeout with how many paths it will scan, so
|
|
522
|
+
* configuring many auditPaths (or a large per-path auditTimeoutMs) doesn't
|
|
523
|
+
* spuriously time out the whole batch against a fixed budget — the same
|
|
524
|
+
* failure mode calculateSyncTimeoutMs fixes for dependency sync.
|
|
525
|
+
*/
|
|
526
|
+
private calculateMultiPathAuditTimeoutMs;
|
|
441
527
|
private performMultiPathAudit;
|
|
442
528
|
/**
|
|
443
529
|
* Parse npm audit JSON output into vulnerability items
|
|
@@ -512,7 +598,8 @@ declare class MonitorClient {
|
|
|
512
598
|
/**
|
|
513
599
|
* Measure CPU utilization by sampling cpus() twice with a 3s gap.
|
|
514
600
|
* The cumulative counters make this the average utilization over the
|
|
515
|
-
* window, across all cores of the host. Returns a percentage (0–100)
|
|
601
|
+
* window, across all cores of the host. Returns a percentage (0–100)
|
|
602
|
+
* plus the CPU model, read off the same samples at zero extra cost.
|
|
516
603
|
*/
|
|
517
604
|
private measureCpuPercent;
|
|
518
605
|
/**
|
|
@@ -531,6 +618,7 @@ declare class MonitorClient {
|
|
|
531
618
|
submitSystemMetric(metric: {
|
|
532
619
|
hostname: string;
|
|
533
620
|
cpuPercent: number;
|
|
621
|
+
cpuModel?: string | null;
|
|
534
622
|
memoryTotal: number;
|
|
535
623
|
memoryUsed: number;
|
|
536
624
|
memoryPercent: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -217,6 +217,7 @@ declare class MonitorClient {
|
|
|
217
217
|
private lastScanTime;
|
|
218
218
|
private lastKnownScanRequestedAt;
|
|
219
219
|
private lastKnownTechScanRequestedAt;
|
|
220
|
+
private currentAuditIntervalHours;
|
|
220
221
|
private versionCheckEnabled;
|
|
221
222
|
private auditTimeoutMs;
|
|
222
223
|
private registryTimeoutMs;
|
|
@@ -260,6 +261,21 @@ declare class MonitorClient {
|
|
|
260
261
|
flush(): Promise<void>;
|
|
261
262
|
private getErrorKey;
|
|
262
263
|
close(): Promise<void>;
|
|
264
|
+
/**
|
|
265
|
+
* Node's fetch() (undici) wraps network-level failures (ECONNREFUSED, DNS lookup
|
|
266
|
+
* failures, TLS errors, ...) in a generic `TypeError: fetch failed`, with the real
|
|
267
|
+
* reason attached via the standard `cause` chain. Without unwrapping it, every
|
|
268
|
+
* network failure across the SDK is reported as the same opaque "fetch failed"
|
|
269
|
+
* message, with no way to diagnose it from the dashboard.
|
|
270
|
+
*/
|
|
271
|
+
private describeErrorCause;
|
|
272
|
+
private truncateDescription;
|
|
273
|
+
/**
|
|
274
|
+
* Run `fn`, swallowing any throw and returning undefined instead. Used to read
|
|
275
|
+
* possibly-hostile Error fields (a throwing message/stack/cause getter) without
|
|
276
|
+
* losing whatever else was already read — each call degrades independently.
|
|
277
|
+
*/
|
|
278
|
+
private safe;
|
|
263
279
|
/**
|
|
264
280
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
265
281
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -269,6 +285,7 @@ declare class MonitorClient {
|
|
|
269
285
|
private startSdkErrorFlushTimer;
|
|
270
286
|
private stopSdkErrorFlushTimer;
|
|
271
287
|
private flushSdkErrors;
|
|
288
|
+
private clearAuditIntervalTimer;
|
|
272
289
|
private stopAuditIntervalTimer;
|
|
273
290
|
/**
|
|
274
291
|
* Fetch project settings from the monitoring server.
|
|
@@ -292,6 +309,12 @@ declare class MonitorClient {
|
|
|
292
309
|
* Also sets up polling for on-demand scan requests from the server.
|
|
293
310
|
*/
|
|
294
311
|
private setupAutoAudit;
|
|
312
|
+
/**
|
|
313
|
+
* (Re)configure the recurring audit timer for the given interval, replacing
|
|
314
|
+
* whichever timer is currently running (if any). intervalHours <= 0 leaves
|
|
315
|
+
* scanning disabled.
|
|
316
|
+
*/
|
|
317
|
+
private setAuditInterval;
|
|
295
318
|
/**
|
|
296
319
|
* Run a vulnerability scan and track the time it was run.
|
|
297
320
|
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
@@ -317,6 +340,55 @@ declare class MonitorClient {
|
|
|
317
340
|
private startFlushTimer;
|
|
318
341
|
private stopFlushTimer;
|
|
319
342
|
syncDependencies(): Promise<void>;
|
|
343
|
+
/**
|
|
344
|
+
* How many sequential registry batches a version-fetch over this many packages
|
|
345
|
+
* will take, given the shared concurrency limit. Shared by fetchLatestVersions
|
|
346
|
+
* (which does the batching) and calculateSyncTimeoutMs (which budgets for it),
|
|
347
|
+
* so the two can't drift out of sync with each other.
|
|
348
|
+
*/
|
|
349
|
+
private registryBatchCount;
|
|
350
|
+
/**
|
|
351
|
+
* Estimate a timeout budget that scales with how many roughly-equal-cost units of
|
|
352
|
+
* sequential work will run (registry batches, audit paths, ...), clamped to a
|
|
353
|
+
* floor/ceiling. Shared by calculateSyncTimeoutMs and calculateMultiPathAuditTimeoutMs
|
|
354
|
+
* so the two scaling formulas can't drift apart. At count=0 this always collapses to
|
|
355
|
+
* floorMs, since overheadMs alone never exceeds a floor sized for real work.
|
|
356
|
+
*/
|
|
357
|
+
private estimateScaledTimeoutMs;
|
|
358
|
+
/**
|
|
359
|
+
* Scale the dependency sync timeout with how many registry lookups AND how many
|
|
360
|
+
* sequential POSTs (one per source, via sendTechnologiesWithEnvironment) it will
|
|
361
|
+
* make, so large dependency lists, many dependencySources, or a large requestTimeoutMs
|
|
362
|
+
* don't spuriously time out against a fixed budget.
|
|
363
|
+
*
|
|
364
|
+
* The POST-phase term (sources.length * requestTimeoutMs) is included even when
|
|
365
|
+
* version checking is off: performDependencySync still does one sequential POST per
|
|
366
|
+
* source regardless, so a flat overhead that ignores source count would under-budget
|
|
367
|
+
* that phase exactly the way a flat registry-batch estimate would under-budget large
|
|
368
|
+
* dependency lists.
|
|
369
|
+
*
|
|
370
|
+
* Registry batches are summed PER SOURCE (not over the combined total) because
|
|
371
|
+
* performDependencySync/fetchLatestVersions batch each source independently and
|
|
372
|
+
* sequentially — ceil(a/n) + ceil(b/n) can be larger than ceil((a+b)/n), so a
|
|
373
|
+
* single batch count over the combined total would under-estimate the real
|
|
374
|
+
* number of sequential registry round-trips when many small sources are configured.
|
|
375
|
+
*/
|
|
376
|
+
private calculateSyncTimeoutMs;
|
|
377
|
+
/**
|
|
378
|
+
* Reads happen before syncDependencies' timeout/AbortSignal exists (see comment
|
|
379
|
+
* there), so this phase bounds itself with a wall-clock deadline on a budget that
|
|
380
|
+
* scales with how many sources are configured (mirroring calculateSyncTimeoutMs's
|
|
381
|
+
* scaling principle for the network phase). A plain deadline timestamp — not an
|
|
382
|
+
* AbortController — is enough here: readPackageJsonFromPath does purely synchronous,
|
|
383
|
+
* non-cancelable fs calls, so there is nothing for a signal to actually interrupt;
|
|
384
|
+
* it can only ever be checked between iterations, exactly like a deadline check.
|
|
385
|
+
* Once the budget is spent, stop reading further sources rather than always paying
|
|
386
|
+
* the full read cost for every configured dependencySource regardless of how long
|
|
387
|
+
* it's already taken — and report it via reportError, not just a console.warn, so a
|
|
388
|
+
* truncated sync isn't silently indistinguishable from a fully successful one on
|
|
389
|
+
* the dashboard.
|
|
390
|
+
*/
|
|
391
|
+
private loadDependencySources;
|
|
320
392
|
private performDependencySync;
|
|
321
393
|
/**
|
|
322
394
|
* Enrich technologies with latest version information from npm registry.
|
|
@@ -416,6 +488,13 @@ declare class MonitorClient {
|
|
|
416
488
|
* Only works in Node.js environment (not browser/bundled).
|
|
417
489
|
*/
|
|
418
490
|
private runCommandWithTimeout;
|
|
491
|
+
/**
|
|
492
|
+
* `npm audit --json` can exit without emitting valid JSON on stdout — e.g. the
|
|
493
|
+
* registry is unreachable, or a fatal npm error is written to stderr instead.
|
|
494
|
+
* Treat that as a soft failure (like a timeout) rather than throwing, so it
|
|
495
|
+
* doesn't surface as a confusing "Unexpected end of JSON input" SDK_ERROR.
|
|
496
|
+
*/
|
|
497
|
+
private parseNpmAuditJson;
|
|
419
498
|
/**
|
|
420
499
|
* Run npm audit and send results to the monitoring server.
|
|
421
500
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -438,6 +517,13 @@ declare class MonitorClient {
|
|
|
438
517
|
* @returns Combined summary of all audit results
|
|
439
518
|
*/
|
|
440
519
|
auditMultiplePaths(): Promise<MultiAuditSummary | null>;
|
|
520
|
+
/**
|
|
521
|
+
* Scale the multi-path audit timeout with how many paths it will scan, so
|
|
522
|
+
* configuring many auditPaths (or a large per-path auditTimeoutMs) doesn't
|
|
523
|
+
* spuriously time out the whole batch against a fixed budget — the same
|
|
524
|
+
* failure mode calculateSyncTimeoutMs fixes for dependency sync.
|
|
525
|
+
*/
|
|
526
|
+
private calculateMultiPathAuditTimeoutMs;
|
|
441
527
|
private performMultiPathAudit;
|
|
442
528
|
/**
|
|
443
529
|
* Parse npm audit JSON output into vulnerability items
|
|
@@ -512,7 +598,8 @@ declare class MonitorClient {
|
|
|
512
598
|
/**
|
|
513
599
|
* Measure CPU utilization by sampling cpus() twice with a 3s gap.
|
|
514
600
|
* The cumulative counters make this the average utilization over the
|
|
515
|
-
* window, across all cores of the host. Returns a percentage (0–100)
|
|
601
|
+
* window, across all cores of the host. Returns a percentage (0–100)
|
|
602
|
+
* plus the CPU model, read off the same samples at zero extra cost.
|
|
516
603
|
*/
|
|
517
604
|
private measureCpuPercent;
|
|
518
605
|
/**
|
|
@@ -531,6 +618,7 @@ declare class MonitorClient {
|
|
|
531
618
|
submitSystemMetric(metric: {
|
|
532
619
|
hostname: string;
|
|
533
620
|
cpuPercent: number;
|
|
621
|
+
cpuModel?: string | null;
|
|
534
622
|
memoryTotal: number;
|
|
535
623
|
memoryUsed: number;
|
|
536
624
|
memoryPercent: number;
|
package/dist/index.js
CHANGED
|
@@ -64,9 +64,17 @@ var CONFIG_LIMITS = {
|
|
|
64
64
|
TECH_VERSION_FETCH_TIMEOUT_MS: 3e4,
|
|
65
65
|
// 30 seconds max for all version fetches
|
|
66
66
|
SYNC_DEPENDENCIES_TIMEOUT_MS: 6e4,
|
|
67
|
-
//
|
|
67
|
+
// floor: minimum timeout for all dependency syncs
|
|
68
|
+
MAX_SYNC_DEPENDENCIES_TIMEOUT_MS: 3e5,
|
|
69
|
+
// ceiling: 5 minutes, even for very large dependency lists
|
|
70
|
+
SYNC_FIXED_OVERHEAD_MS: 1e4,
|
|
71
|
+
// buffer per sync for local reads + POSTs to the server
|
|
68
72
|
AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
|
|
69
|
-
//
|
|
73
|
+
// floor: minimum timeout for all audit paths
|
|
74
|
+
MAX_AUDIT_MULTI_PATH_TIMEOUT_MS: 18e5,
|
|
75
|
+
// ceiling: 30 minutes, even for many audit paths
|
|
76
|
+
AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS: 1e4,
|
|
77
|
+
// buffer per multi-path audit for local reads + POSTs to the server
|
|
70
78
|
REGISTRY_CONCURRENCY_LIMIT: 5,
|
|
71
79
|
// Limit parallel requests to avoid rate limiting
|
|
72
80
|
HEALTH_CHECK_FETCH_INTERVAL_MS: 6e4,
|
|
@@ -90,6 +98,9 @@ var MonitorClient = class {
|
|
|
90
98
|
this.lastScanTime = null;
|
|
91
99
|
this.lastKnownScanRequestedAt = null;
|
|
92
100
|
this.lastKnownTechScanRequestedAt = null;
|
|
101
|
+
// Interval (in hours) the currently-running auditIntervalTimer was configured with,
|
|
102
|
+
// so checkForScanRequest() can detect server-side changes and reconfigure it.
|
|
103
|
+
this.currentAuditIntervalHours = null;
|
|
93
104
|
this.healthCheckFetchTimer = null;
|
|
94
105
|
this.healthCheckTimers = /* @__PURE__ */ new Map();
|
|
95
106
|
this.healthCheckResultsQueue = [];
|
|
@@ -338,6 +349,46 @@ var MonitorClient = class {
|
|
|
338
349
|
await this.flushHealthResults();
|
|
339
350
|
await this.flushSdkErrors();
|
|
340
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Node's fetch() (undici) wraps network-level failures (ECONNREFUSED, DNS lookup
|
|
354
|
+
* failures, TLS errors, ...) in a generic `TypeError: fetch failed`, with the real
|
|
355
|
+
* reason attached via the standard `cause` chain. Without unwrapping it, every
|
|
356
|
+
* network failure across the SDK is reported as the same opaque "fetch failed"
|
|
357
|
+
* message, with no way to diagnose it from the dashboard.
|
|
358
|
+
*/
|
|
359
|
+
describeErrorCause(err, depth = 0) {
|
|
360
|
+
if (depth > 3) return void 0;
|
|
361
|
+
const cause = err.cause;
|
|
362
|
+
if (cause === void 0 || cause === null) return void 0;
|
|
363
|
+
const aggregateErrors = cause.errors;
|
|
364
|
+
if (Array.isArray(aggregateErrors)) {
|
|
365
|
+
const childSummary = aggregateErrors.map((e) => e instanceof Error ? e.message : String(e)).join("; ");
|
|
366
|
+
const summary = cause instanceof Error && cause.message ? `${cause.message} (${childSummary})` : childSummary;
|
|
367
|
+
return this.truncateDescription(summary);
|
|
368
|
+
}
|
|
369
|
+
if (cause instanceof Error) {
|
|
370
|
+
const nested = this.describeErrorCause(cause, depth + 1);
|
|
371
|
+
const description = nested ? `${cause.message} (${nested})` : cause.message;
|
|
372
|
+
return this.truncateDescription(description);
|
|
373
|
+
}
|
|
374
|
+
return this.truncateDescription(String(cause));
|
|
375
|
+
}
|
|
376
|
+
truncateDescription(description) {
|
|
377
|
+
const limit = 500;
|
|
378
|
+
return description.length > limit ? `${description.slice(0, limit)}...` : description;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Run `fn`, swallowing any throw and returning undefined instead. Used to read
|
|
382
|
+
* possibly-hostile Error fields (a throwing message/stack/cause getter) without
|
|
383
|
+
* losing whatever else was already read — each call degrades independently.
|
|
384
|
+
*/
|
|
385
|
+
safe(fn) {
|
|
386
|
+
try {
|
|
387
|
+
return fn();
|
|
388
|
+
} catch {
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
341
392
|
/**
|
|
342
393
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
343
394
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -346,8 +397,16 @@ var MonitorClient = class {
|
|
|
346
397
|
reportError(category, message, err) {
|
|
347
398
|
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
348
399
|
this.sdkErrorsInCurrentWindow++;
|
|
349
|
-
|
|
350
|
-
|
|
400
|
+
let errorMessage = message;
|
|
401
|
+
let stack;
|
|
402
|
+
if (err instanceof Error) {
|
|
403
|
+
errorMessage = this.safe(() => `${message}: ${err.message}`) ?? message;
|
|
404
|
+
const causeDescription = this.safe(() => this.describeErrorCause(err));
|
|
405
|
+
if (causeDescription) {
|
|
406
|
+
errorMessage += ` (cause: ${causeDescription})`;
|
|
407
|
+
}
|
|
408
|
+
stack = this.safe(() => err.stack);
|
|
409
|
+
}
|
|
351
410
|
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
352
411
|
if (this.sdkErrorQueue.length >= 20) {
|
|
353
412
|
this.flushSdkErrors().catch(() => {
|
|
@@ -393,11 +452,14 @@ var MonitorClient = class {
|
|
|
393
452
|
console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
|
|
394
453
|
}
|
|
395
454
|
}
|
|
396
|
-
|
|
455
|
+
clearAuditIntervalTimer() {
|
|
397
456
|
if (this.auditIntervalTimer) {
|
|
398
457
|
clearInterval(this.auditIntervalTimer);
|
|
399
458
|
this.auditIntervalTimer = null;
|
|
400
459
|
}
|
|
460
|
+
}
|
|
461
|
+
stopAuditIntervalTimer() {
|
|
462
|
+
this.clearAuditIntervalTimer();
|
|
401
463
|
if (this.settingsPollingTimer) {
|
|
402
464
|
clearInterval(this.settingsPollingTimer);
|
|
403
465
|
this.settingsPollingTimer = null;
|
|
@@ -447,15 +509,11 @@ var MonitorClient = class {
|
|
|
447
509
|
const intervalHours = settings.vulnerabilityScanIntervalHours;
|
|
448
510
|
if (intervalHours <= 0) {
|
|
449
511
|
console.log("[MonitorClient] Scheduled vulnerability scanning disabled by server configuration");
|
|
512
|
+
this.currentAuditIntervalHours = intervalHours;
|
|
450
513
|
} else {
|
|
451
514
|
console.log(`[MonitorClient] Auto vulnerability scanning enabled (every ${intervalHours} hours)`);
|
|
452
515
|
await this.runScanAndTrackTime();
|
|
453
|
-
|
|
454
|
-
this.auditIntervalTimer = setInterval(() => {
|
|
455
|
-
this.runScanAndTrackTime().catch((err) => {
|
|
456
|
-
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
457
|
-
});
|
|
458
|
-
}, intervalMs);
|
|
516
|
+
this.setAuditInterval(intervalHours);
|
|
459
517
|
}
|
|
460
518
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
461
519
|
this.settingsPollingTimer = setInterval(() => {
|
|
@@ -464,6 +522,24 @@ var MonitorClient = class {
|
|
|
464
522
|
});
|
|
465
523
|
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
466
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* (Re)configure the recurring audit timer for the given interval, replacing
|
|
527
|
+
* whichever timer is currently running (if any). intervalHours <= 0 leaves
|
|
528
|
+
* scanning disabled.
|
|
529
|
+
*/
|
|
530
|
+
setAuditInterval(intervalHours) {
|
|
531
|
+
this.clearAuditIntervalTimer();
|
|
532
|
+
this.currentAuditIntervalHours = intervalHours;
|
|
533
|
+
if (intervalHours <= 0) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
537
|
+
this.auditIntervalTimer = setInterval(() => {
|
|
538
|
+
this.runScanAndTrackTime().catch((err) => {
|
|
539
|
+
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
540
|
+
});
|
|
541
|
+
}, intervalMs);
|
|
542
|
+
}
|
|
467
543
|
/**
|
|
468
544
|
* Run a vulnerability scan and track the time it was run.
|
|
469
545
|
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
@@ -507,6 +583,12 @@ var MonitorClient = class {
|
|
|
507
583
|
await this.syncDependencies();
|
|
508
584
|
}
|
|
509
585
|
}
|
|
586
|
+
if (settings.vulnerabilityScanIntervalHours !== this.currentAuditIntervalHours) {
|
|
587
|
+
console.log(
|
|
588
|
+
`[MonitorClient] Auto scan interval changed (${this.currentAuditIntervalHours ?? "unset"} \u2192 ${settings.vulnerabilityScanIntervalHours} hours), reconfiguring`
|
|
589
|
+
);
|
|
590
|
+
this.setAuditInterval(settings.vulnerabilityScanIntervalHours);
|
|
591
|
+
}
|
|
510
592
|
} catch (err) {
|
|
511
593
|
this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
|
|
512
594
|
}
|
|
@@ -627,9 +709,11 @@ var MonitorClient = class {
|
|
|
627
709
|
async syncDependencies() {
|
|
628
710
|
console.log("[MonitorClient] Starting technology sync...");
|
|
629
711
|
try {
|
|
712
|
+
const sources = await this.loadDependencySources();
|
|
713
|
+
const timeoutMs = this.calculateSyncTimeoutMs(sources);
|
|
630
714
|
await this.withTimeout(
|
|
631
|
-
(signal) => this.performDependencySync(signal),
|
|
632
|
-
|
|
715
|
+
(signal) => this.performDependencySync(sources, signal),
|
|
716
|
+
timeoutMs,
|
|
633
717
|
"Technology sync timed out"
|
|
634
718
|
);
|
|
635
719
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
@@ -637,25 +721,100 @@ var MonitorClient = class {
|
|
|
637
721
|
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
638
722
|
}
|
|
639
723
|
}
|
|
640
|
-
|
|
724
|
+
/**
|
|
725
|
+
* How many sequential registry batches a version-fetch over this many packages
|
|
726
|
+
* will take, given the shared concurrency limit. Shared by fetchLatestVersions
|
|
727
|
+
* (which does the batching) and calculateSyncTimeoutMs (which budgets for it),
|
|
728
|
+
* so the two can't drift out of sync with each other.
|
|
729
|
+
*/
|
|
730
|
+
registryBatchCount(itemCount) {
|
|
731
|
+
return Math.ceil(itemCount / CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Estimate a timeout budget that scales with how many roughly-equal-cost units of
|
|
735
|
+
* sequential work will run (registry batches, audit paths, ...), clamped to a
|
|
736
|
+
* floor/ceiling. Shared by calculateSyncTimeoutMs and calculateMultiPathAuditTimeoutMs
|
|
737
|
+
* so the two scaling formulas can't drift apart. At count=0 this always collapses to
|
|
738
|
+
* floorMs, since overheadMs alone never exceeds a floor sized for real work.
|
|
739
|
+
*/
|
|
740
|
+
estimateScaledTimeoutMs(count, perItemMs, overheadMs, floorMs, ceilingMs) {
|
|
741
|
+
const estimatedMs = count * perItemMs + overheadMs;
|
|
742
|
+
return Math.min(ceilingMs, Math.max(floorMs, estimatedMs));
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Scale the dependency sync timeout with how many registry lookups AND how many
|
|
746
|
+
* sequential POSTs (one per source, via sendTechnologiesWithEnvironment) it will
|
|
747
|
+
* make, so large dependency lists, many dependencySources, or a large requestTimeoutMs
|
|
748
|
+
* don't spuriously time out against a fixed budget.
|
|
749
|
+
*
|
|
750
|
+
* The POST-phase term (sources.length * requestTimeoutMs) is included even when
|
|
751
|
+
* version checking is off: performDependencySync still does one sequential POST per
|
|
752
|
+
* source regardless, so a flat overhead that ignores source count would under-budget
|
|
753
|
+
* that phase exactly the way a flat registry-batch estimate would under-budget large
|
|
754
|
+
* dependency lists.
|
|
755
|
+
*
|
|
756
|
+
* Registry batches are summed PER SOURCE (not over the combined total) because
|
|
757
|
+
* performDependencySync/fetchLatestVersions batch each source independently and
|
|
758
|
+
* sequentially — ceil(a/n) + ceil(b/n) can be larger than ceil((a+b)/n), so a
|
|
759
|
+
* single batch count over the combined total would under-estimate the real
|
|
760
|
+
* number of sequential registry round-trips when many small sources are configured.
|
|
761
|
+
*/
|
|
762
|
+
calculateSyncTimeoutMs(sources) {
|
|
763
|
+
const postPhaseMs = sources.length * this.requestTimeoutMs;
|
|
764
|
+
const batches = this.versionCheckEnabled ? sources.reduce((sum, source) => sum + this.registryBatchCount(source.technologies.length), 0) : 0;
|
|
765
|
+
return this.estimateScaledTimeoutMs(
|
|
766
|
+
batches,
|
|
767
|
+
this.registryTimeoutMs,
|
|
768
|
+
CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS + postPhaseMs,
|
|
769
|
+
CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
|
|
770
|
+
CONFIG_LIMITS.MAX_SYNC_DEPENDENCIES_TIMEOUT_MS
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Reads happen before syncDependencies' timeout/AbortSignal exists (see comment
|
|
775
|
+
* there), so this phase bounds itself with a wall-clock deadline on a budget that
|
|
776
|
+
* scales with how many sources are configured (mirroring calculateSyncTimeoutMs's
|
|
777
|
+
* scaling principle for the network phase). A plain deadline timestamp — not an
|
|
778
|
+
* AbortController — is enough here: readPackageJsonFromPath does purely synchronous,
|
|
779
|
+
* non-cancelable fs calls, so there is nothing for a signal to actually interrupt;
|
|
780
|
+
* it can only ever be checked between iterations, exactly like a deadline check.
|
|
781
|
+
* Once the budget is spent, stop reading further sources rather than always paying
|
|
782
|
+
* the full read cost for every configured dependencySource regardless of how long
|
|
783
|
+
* it's already taken — and report it via reportError, not just a console.warn, so a
|
|
784
|
+
* truncated sync isn't silently indistinguishable from a fully successful one on
|
|
785
|
+
* the dashboard.
|
|
786
|
+
*/
|
|
787
|
+
async loadDependencySources() {
|
|
641
788
|
if (this.dependencySources && this.dependencySources.length > 0) {
|
|
789
|
+
const sources = [];
|
|
790
|
+
const readBudgetMs = CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS * this.dependencySources.length;
|
|
791
|
+
const deadline = Date.now() + readBudgetMs;
|
|
642
792
|
for (const source of this.dependencySources) {
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
793
|
+
if (Date.now() >= deadline) {
|
|
794
|
+
const skippedCount = this.dependencySources.length - sources.length;
|
|
795
|
+
const warning = `Dependency source reads exceeded ${readBudgetMs}ms, skipping remaining ${skippedCount} source(s)`;
|
|
796
|
+
console.warn(`[MonitorClient] ${warning}`);
|
|
797
|
+
this.reportError("DEPENDENCY_SYNC", warning);
|
|
798
|
+
break;
|
|
646
799
|
}
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
|
|
650
|
-
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
651
|
-
console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
|
|
800
|
+
const technologies2 = await this.readPackageJsonFromPath(source.path);
|
|
801
|
+
sources.push({ environment: source.environment, technologies: technologies2 });
|
|
652
802
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
803
|
+
return sources;
|
|
804
|
+
}
|
|
805
|
+
const technologies = await this.readPackageJson();
|
|
806
|
+
return [{ environment: this.environment, technologies }];
|
|
807
|
+
}
|
|
808
|
+
async performDependencySync(sources, signal) {
|
|
809
|
+
for (const source of sources) {
|
|
810
|
+
if (signal?.aborted) {
|
|
811
|
+
console.log("[MonitorClient] Technology sync cancelled");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (source.technologies.length === 0) continue;
|
|
815
|
+
const enrichedTechnologies = await this.enrichWithLatestVersions(source.technologies, signal);
|
|
816
|
+
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
817
|
+
console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
|
|
659
818
|
}
|
|
660
819
|
}
|
|
661
820
|
/**
|
|
@@ -769,13 +928,22 @@ var MonitorClient = class {
|
|
|
769
928
|
try {
|
|
770
929
|
if (signal?.aborted) return null;
|
|
771
930
|
const encodedName = encodeURIComponent(packageName).replace("%40", "@");
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
931
|
+
const requestController = new AbortController();
|
|
932
|
+
const timeoutId = setTimeout(() => requestController.abort(), this.registryTimeoutMs);
|
|
933
|
+
const onOuterAbort = () => requestController.abort();
|
|
934
|
+
signal?.addEventListener("abort", onOuterAbort);
|
|
935
|
+
try {
|
|
936
|
+
const response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
|
|
937
|
+
headers: { "Accept": "application/json" },
|
|
938
|
+
signal: requestController.signal
|
|
939
|
+
});
|
|
940
|
+
if (!response.ok) return null;
|
|
941
|
+
const data = await response.json();
|
|
942
|
+
return data["dist-tags"]?.latest || null;
|
|
943
|
+
} finally {
|
|
944
|
+
clearTimeout(timeoutId);
|
|
945
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
946
|
+
}
|
|
779
947
|
} catch {
|
|
780
948
|
return null;
|
|
781
949
|
}
|
|
@@ -788,7 +956,7 @@ var MonitorClient = class {
|
|
|
788
956
|
async fetchLatestVersions(packageNames, signal) {
|
|
789
957
|
const results = /* @__PURE__ */ new Map();
|
|
790
958
|
const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
|
|
791
|
-
const totalBatches =
|
|
959
|
+
const totalBatches = this.registryBatchCount(packageNames.length);
|
|
792
960
|
for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
|
|
793
961
|
if (signal?.aborted) {
|
|
794
962
|
console.log("[MonitorClient] Version fetch cancelled");
|
|
@@ -959,7 +1127,7 @@ var MonitorClient = class {
|
|
|
959
1127
|
"child_process"
|
|
960
1128
|
);
|
|
961
1129
|
const { spawn } = childProcess;
|
|
962
|
-
return new Promise((resolve) => {
|
|
1130
|
+
return new Promise((resolve, reject) => {
|
|
963
1131
|
const proc = spawn(command, args, {
|
|
964
1132
|
cwd: options.cwd,
|
|
965
1133
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -968,10 +1136,8 @@ var MonitorClient = class {
|
|
|
968
1136
|
let stdout = "";
|
|
969
1137
|
let stderr = "";
|
|
970
1138
|
let timedOut = false;
|
|
971
|
-
let killed = false;
|
|
972
1139
|
const timeoutId = setTimeout(() => {
|
|
973
1140
|
timedOut = true;
|
|
974
|
-
killed = true;
|
|
975
1141
|
proc.kill("SIGTERM");
|
|
976
1142
|
setTimeout(() => {
|
|
977
1143
|
if (!proc.killed) {
|
|
@@ -991,14 +1157,38 @@ var MonitorClient = class {
|
|
|
991
1157
|
});
|
|
992
1158
|
proc.on("close", () => {
|
|
993
1159
|
clearTimeout(timeoutId);
|
|
994
|
-
resolve({ stdout, timedOut });
|
|
1160
|
+
resolve({ stdout, stderr, timedOut });
|
|
995
1161
|
});
|
|
996
|
-
proc.on("error", () => {
|
|
1162
|
+
proc.on("error", (err) => {
|
|
997
1163
|
clearTimeout(timeoutId);
|
|
998
|
-
|
|
1164
|
+
reject(err);
|
|
999
1165
|
});
|
|
1000
1166
|
});
|
|
1001
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* `npm audit --json` can exit without emitting valid JSON on stdout — e.g. the
|
|
1170
|
+
* registry is unreachable, or a fatal npm error is written to stderr instead.
|
|
1171
|
+
* Treat that as a soft failure (like a timeout) rather than throwing, so it
|
|
1172
|
+
* doesn't surface as a confusing "Unexpected end of JSON input" SDK_ERROR.
|
|
1173
|
+
*/
|
|
1174
|
+
parseNpmAuditJson(auditOutput, stderr) {
|
|
1175
|
+
const stderrSuffix = () => {
|
|
1176
|
+
const trimmedStderr = stderr.trim();
|
|
1177
|
+
return trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : "";
|
|
1178
|
+
};
|
|
1179
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1180
|
+
console.error(`[MonitorClient] npm audit produced no output${stderrSuffix()}`);
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
return JSON.parse(auditOutput);
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
console.error(
|
|
1187
|
+
`[MonitorClient] npm audit produced invalid JSON: ${err instanceof Error ? err.message : String(err)}${stderrSuffix()}`
|
|
1188
|
+
);
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1002
1192
|
/**
|
|
1003
1193
|
* Run npm audit and send results to the monitoring server.
|
|
1004
1194
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -1075,6 +1265,13 @@ var MonitorClient = class {
|
|
|
1075
1265
|
return null;
|
|
1076
1266
|
}
|
|
1077
1267
|
auditOutput = result2.stdout;
|
|
1268
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1269
|
+
const trimmedStderr = result2.stderr.trim();
|
|
1270
|
+
console.error(
|
|
1271
|
+
`[MonitorClient] yarn audit produced no output${trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : ""}`
|
|
1272
|
+
);
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1078
1275
|
vulnerabilities = this.parseYarnAuditOutput(auditOutput);
|
|
1079
1276
|
const lines = auditOutput.trim().split("\n");
|
|
1080
1277
|
for (const line of lines) {
|
|
@@ -1087,22 +1284,10 @@ var MonitorClient = class {
|
|
|
1087
1284
|
} catch {
|
|
1088
1285
|
}
|
|
1089
1286
|
}
|
|
1090
|
-
} else if (packageManager === "pnpm") {
|
|
1091
|
-
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1092
|
-
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1093
|
-
cwd: projectPath,
|
|
1094
|
-
timeout: this.auditTimeoutMs,
|
|
1095
|
-
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1096
|
-
});
|
|
1097
|
-
if (result2.timedOut) {
|
|
1098
|
-
console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
|
|
1099
|
-
return null;
|
|
1100
|
-
}
|
|
1101
|
-
auditOutput = result2.stdout;
|
|
1102
|
-
const auditData = JSON.parse(auditOutput);
|
|
1103
|
-
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1104
|
-
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1105
1287
|
} else {
|
|
1288
|
+
if (packageManager === "pnpm") {
|
|
1289
|
+
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1290
|
+
}
|
|
1106
1291
|
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1107
1292
|
cwd: projectPath,
|
|
1108
1293
|
timeout: this.auditTimeoutMs,
|
|
@@ -1113,7 +1298,8 @@ var MonitorClient = class {
|
|
|
1113
1298
|
return null;
|
|
1114
1299
|
}
|
|
1115
1300
|
auditOutput = result2.stdout;
|
|
1116
|
-
const auditData =
|
|
1301
|
+
const auditData = this.parseNpmAuditJson(auditOutput, result2.stderr);
|
|
1302
|
+
if (!auditData) return null;
|
|
1117
1303
|
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1118
1304
|
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1119
1305
|
}
|
|
@@ -1158,9 +1344,10 @@ var MonitorClient = class {
|
|
|
1158
1344
|
}
|
|
1159
1345
|
console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
|
|
1160
1346
|
try {
|
|
1347
|
+
const timeoutMs = this.calculateMultiPathAuditTimeoutMs(this.auditPaths.length);
|
|
1161
1348
|
const result = await this.withTimeout(
|
|
1162
1349
|
() => this.performMultiPathAudit(),
|
|
1163
|
-
|
|
1350
|
+
timeoutMs,
|
|
1164
1351
|
"Multi-path audit timed out"
|
|
1165
1352
|
);
|
|
1166
1353
|
if (result) {
|
|
@@ -1172,6 +1359,21 @@ var MonitorClient = class {
|
|
|
1172
1359
|
return null;
|
|
1173
1360
|
}
|
|
1174
1361
|
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Scale the multi-path audit timeout with how many paths it will scan, so
|
|
1364
|
+
* configuring many auditPaths (or a large per-path auditTimeoutMs) doesn't
|
|
1365
|
+
* spuriously time out the whole batch against a fixed budget — the same
|
|
1366
|
+
* failure mode calculateSyncTimeoutMs fixes for dependency sync.
|
|
1367
|
+
*/
|
|
1368
|
+
calculateMultiPathAuditTimeoutMs(pathCount) {
|
|
1369
|
+
return this.estimateScaledTimeoutMs(
|
|
1370
|
+
pathCount,
|
|
1371
|
+
this.auditTimeoutMs,
|
|
1372
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS,
|
|
1373
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
|
|
1374
|
+
CONFIG_LIMITS.MAX_AUDIT_MULTI_PATH_TIMEOUT_MS
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1175
1377
|
async performMultiPathAudit() {
|
|
1176
1378
|
if (!this.auditPaths) return null;
|
|
1177
1379
|
const results = [];
|
|
@@ -1594,7 +1796,7 @@ var MonitorClient = class {
|
|
|
1594
1796
|
}
|
|
1595
1797
|
try {
|
|
1596
1798
|
const hostname = os.hostname();
|
|
1597
|
-
const cpuPercent = await this.measureCpuPercent(os);
|
|
1799
|
+
const { percent: cpuPercent, model: cpuModel } = await this.measureCpuPercent(os);
|
|
1598
1800
|
const memoryTotal = os.totalmem();
|
|
1599
1801
|
const memoryAvailable = await this.getAvailableMemoryBytes(os, fs);
|
|
1600
1802
|
const memoryUsed = memoryTotal - memoryAvailable;
|
|
@@ -1603,6 +1805,7 @@ var MonitorClient = class {
|
|
|
1603
1805
|
await this.submitSystemMetric({
|
|
1604
1806
|
hostname,
|
|
1605
1807
|
cpuPercent,
|
|
1808
|
+
cpuModel,
|
|
1606
1809
|
memoryTotal,
|
|
1607
1810
|
memoryUsed,
|
|
1608
1811
|
memoryPercent,
|
|
@@ -1615,12 +1818,14 @@ var MonitorClient = class {
|
|
|
1615
1818
|
/**
|
|
1616
1819
|
* Measure CPU utilization by sampling cpus() twice with a 3s gap.
|
|
1617
1820
|
* The cumulative counters make this the average utilization over the
|
|
1618
|
-
* window, across all cores of the host. Returns a percentage (0–100)
|
|
1821
|
+
* window, across all cores of the host. Returns a percentage (0–100)
|
|
1822
|
+
* plus the CPU model, read off the same samples at zero extra cost.
|
|
1619
1823
|
*/
|
|
1620
1824
|
async measureCpuPercent(os) {
|
|
1621
1825
|
const sample1 = os.cpus();
|
|
1622
1826
|
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
1623
1827
|
const sample2 = os.cpus();
|
|
1828
|
+
const model = sample1[0]?.model ?? null;
|
|
1624
1829
|
let totalIdle = 0;
|
|
1625
1830
|
let totalTick = 0;
|
|
1626
1831
|
for (let i = 0; i < sample2.length; i++) {
|
|
@@ -1632,10 +1837,10 @@ var MonitorClient = class {
|
|
|
1632
1837
|
totalIdle += curr.times.idle - prev.times.idle;
|
|
1633
1838
|
}
|
|
1634
1839
|
if (totalTick <= 0) {
|
|
1635
|
-
return 0;
|
|
1840
|
+
return { percent: 0, model };
|
|
1636
1841
|
}
|
|
1637
1842
|
const idlePercent = totalIdle / totalTick * 100;
|
|
1638
|
-
return Math.max(0, Math.min(100, 100 - idlePercent));
|
|
1843
|
+
return { percent: Math.max(0, Math.min(100, 100 - idlePercent)), model };
|
|
1639
1844
|
}
|
|
1640
1845
|
/**
|
|
1641
1846
|
* Get reclaimable memory in bytes. On Linux, reads MemAvailable from
|
package/dist/index.mjs
CHANGED
|
@@ -28,9 +28,17 @@ var CONFIG_LIMITS = {
|
|
|
28
28
|
TECH_VERSION_FETCH_TIMEOUT_MS: 3e4,
|
|
29
29
|
// 30 seconds max for all version fetches
|
|
30
30
|
SYNC_DEPENDENCIES_TIMEOUT_MS: 6e4,
|
|
31
|
-
//
|
|
31
|
+
// floor: minimum timeout for all dependency syncs
|
|
32
|
+
MAX_SYNC_DEPENDENCIES_TIMEOUT_MS: 3e5,
|
|
33
|
+
// ceiling: 5 minutes, even for very large dependency lists
|
|
34
|
+
SYNC_FIXED_OVERHEAD_MS: 1e4,
|
|
35
|
+
// buffer per sync for local reads + POSTs to the server
|
|
32
36
|
AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
|
|
33
|
-
//
|
|
37
|
+
// floor: minimum timeout for all audit paths
|
|
38
|
+
MAX_AUDIT_MULTI_PATH_TIMEOUT_MS: 18e5,
|
|
39
|
+
// ceiling: 30 minutes, even for many audit paths
|
|
40
|
+
AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS: 1e4,
|
|
41
|
+
// buffer per multi-path audit for local reads + POSTs to the server
|
|
34
42
|
REGISTRY_CONCURRENCY_LIMIT: 5,
|
|
35
43
|
// Limit parallel requests to avoid rate limiting
|
|
36
44
|
HEALTH_CHECK_FETCH_INTERVAL_MS: 6e4,
|
|
@@ -54,6 +62,9 @@ var MonitorClient = class {
|
|
|
54
62
|
this.lastScanTime = null;
|
|
55
63
|
this.lastKnownScanRequestedAt = null;
|
|
56
64
|
this.lastKnownTechScanRequestedAt = null;
|
|
65
|
+
// Interval (in hours) the currently-running auditIntervalTimer was configured with,
|
|
66
|
+
// so checkForScanRequest() can detect server-side changes and reconfigure it.
|
|
67
|
+
this.currentAuditIntervalHours = null;
|
|
57
68
|
this.healthCheckFetchTimer = null;
|
|
58
69
|
this.healthCheckTimers = /* @__PURE__ */ new Map();
|
|
59
70
|
this.healthCheckResultsQueue = [];
|
|
@@ -302,6 +313,46 @@ var MonitorClient = class {
|
|
|
302
313
|
await this.flushHealthResults();
|
|
303
314
|
await this.flushSdkErrors();
|
|
304
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Node's fetch() (undici) wraps network-level failures (ECONNREFUSED, DNS lookup
|
|
318
|
+
* failures, TLS errors, ...) in a generic `TypeError: fetch failed`, with the real
|
|
319
|
+
* reason attached via the standard `cause` chain. Without unwrapping it, every
|
|
320
|
+
* network failure across the SDK is reported as the same opaque "fetch failed"
|
|
321
|
+
* message, with no way to diagnose it from the dashboard.
|
|
322
|
+
*/
|
|
323
|
+
describeErrorCause(err, depth = 0) {
|
|
324
|
+
if (depth > 3) return void 0;
|
|
325
|
+
const cause = err.cause;
|
|
326
|
+
if (cause === void 0 || cause === null) return void 0;
|
|
327
|
+
const aggregateErrors = cause.errors;
|
|
328
|
+
if (Array.isArray(aggregateErrors)) {
|
|
329
|
+
const childSummary = aggregateErrors.map((e) => e instanceof Error ? e.message : String(e)).join("; ");
|
|
330
|
+
const summary = cause instanceof Error && cause.message ? `${cause.message} (${childSummary})` : childSummary;
|
|
331
|
+
return this.truncateDescription(summary);
|
|
332
|
+
}
|
|
333
|
+
if (cause instanceof Error) {
|
|
334
|
+
const nested = this.describeErrorCause(cause, depth + 1);
|
|
335
|
+
const description = nested ? `${cause.message} (${nested})` : cause.message;
|
|
336
|
+
return this.truncateDescription(description);
|
|
337
|
+
}
|
|
338
|
+
return this.truncateDescription(String(cause));
|
|
339
|
+
}
|
|
340
|
+
truncateDescription(description) {
|
|
341
|
+
const limit = 500;
|
|
342
|
+
return description.length > limit ? `${description.slice(0, limit)}...` : description;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Run `fn`, swallowing any throw and returning undefined instead. Used to read
|
|
346
|
+
* possibly-hostile Error fields (a throwing message/stack/cause getter) without
|
|
347
|
+
* losing whatever else was already read — each call degrades independently.
|
|
348
|
+
*/
|
|
349
|
+
safe(fn) {
|
|
350
|
+
try {
|
|
351
|
+
return fn();
|
|
352
|
+
} catch {
|
|
353
|
+
return void 0;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
305
356
|
/**
|
|
306
357
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
307
358
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -310,8 +361,16 @@ var MonitorClient = class {
|
|
|
310
361
|
reportError(category, message, err) {
|
|
311
362
|
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
312
363
|
this.sdkErrorsInCurrentWindow++;
|
|
313
|
-
|
|
314
|
-
|
|
364
|
+
let errorMessage = message;
|
|
365
|
+
let stack;
|
|
366
|
+
if (err instanceof Error) {
|
|
367
|
+
errorMessage = this.safe(() => `${message}: ${err.message}`) ?? message;
|
|
368
|
+
const causeDescription = this.safe(() => this.describeErrorCause(err));
|
|
369
|
+
if (causeDescription) {
|
|
370
|
+
errorMessage += ` (cause: ${causeDescription})`;
|
|
371
|
+
}
|
|
372
|
+
stack = this.safe(() => err.stack);
|
|
373
|
+
}
|
|
315
374
|
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
316
375
|
if (this.sdkErrorQueue.length >= 20) {
|
|
317
376
|
this.flushSdkErrors().catch(() => {
|
|
@@ -357,11 +416,14 @@ var MonitorClient = class {
|
|
|
357
416
|
console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
|
|
358
417
|
}
|
|
359
418
|
}
|
|
360
|
-
|
|
419
|
+
clearAuditIntervalTimer() {
|
|
361
420
|
if (this.auditIntervalTimer) {
|
|
362
421
|
clearInterval(this.auditIntervalTimer);
|
|
363
422
|
this.auditIntervalTimer = null;
|
|
364
423
|
}
|
|
424
|
+
}
|
|
425
|
+
stopAuditIntervalTimer() {
|
|
426
|
+
this.clearAuditIntervalTimer();
|
|
365
427
|
if (this.settingsPollingTimer) {
|
|
366
428
|
clearInterval(this.settingsPollingTimer);
|
|
367
429
|
this.settingsPollingTimer = null;
|
|
@@ -411,15 +473,11 @@ var MonitorClient = class {
|
|
|
411
473
|
const intervalHours = settings.vulnerabilityScanIntervalHours;
|
|
412
474
|
if (intervalHours <= 0) {
|
|
413
475
|
console.log("[MonitorClient] Scheduled vulnerability scanning disabled by server configuration");
|
|
476
|
+
this.currentAuditIntervalHours = intervalHours;
|
|
414
477
|
} else {
|
|
415
478
|
console.log(`[MonitorClient] Auto vulnerability scanning enabled (every ${intervalHours} hours)`);
|
|
416
479
|
await this.runScanAndTrackTime();
|
|
417
|
-
|
|
418
|
-
this.auditIntervalTimer = setInterval(() => {
|
|
419
|
-
this.runScanAndTrackTime().catch((err) => {
|
|
420
|
-
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
421
|
-
});
|
|
422
|
-
}, intervalMs);
|
|
480
|
+
this.setAuditInterval(intervalHours);
|
|
423
481
|
}
|
|
424
482
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
425
483
|
this.settingsPollingTimer = setInterval(() => {
|
|
@@ -428,6 +486,24 @@ var MonitorClient = class {
|
|
|
428
486
|
});
|
|
429
487
|
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
430
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* (Re)configure the recurring audit timer for the given interval, replacing
|
|
491
|
+
* whichever timer is currently running (if any). intervalHours <= 0 leaves
|
|
492
|
+
* scanning disabled.
|
|
493
|
+
*/
|
|
494
|
+
setAuditInterval(intervalHours) {
|
|
495
|
+
this.clearAuditIntervalTimer();
|
|
496
|
+
this.currentAuditIntervalHours = intervalHours;
|
|
497
|
+
if (intervalHours <= 0) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
501
|
+
this.auditIntervalTimer = setInterval(() => {
|
|
502
|
+
this.runScanAndTrackTime().catch((err) => {
|
|
503
|
+
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
504
|
+
});
|
|
505
|
+
}, intervalMs);
|
|
506
|
+
}
|
|
431
507
|
/**
|
|
432
508
|
* Run a vulnerability scan and track the time it was run.
|
|
433
509
|
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
@@ -471,6 +547,12 @@ var MonitorClient = class {
|
|
|
471
547
|
await this.syncDependencies();
|
|
472
548
|
}
|
|
473
549
|
}
|
|
550
|
+
if (settings.vulnerabilityScanIntervalHours !== this.currentAuditIntervalHours) {
|
|
551
|
+
console.log(
|
|
552
|
+
`[MonitorClient] Auto scan interval changed (${this.currentAuditIntervalHours ?? "unset"} \u2192 ${settings.vulnerabilityScanIntervalHours} hours), reconfiguring`
|
|
553
|
+
);
|
|
554
|
+
this.setAuditInterval(settings.vulnerabilityScanIntervalHours);
|
|
555
|
+
}
|
|
474
556
|
} catch (err) {
|
|
475
557
|
this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
|
|
476
558
|
}
|
|
@@ -591,9 +673,11 @@ var MonitorClient = class {
|
|
|
591
673
|
async syncDependencies() {
|
|
592
674
|
console.log("[MonitorClient] Starting technology sync...");
|
|
593
675
|
try {
|
|
676
|
+
const sources = await this.loadDependencySources();
|
|
677
|
+
const timeoutMs = this.calculateSyncTimeoutMs(sources);
|
|
594
678
|
await this.withTimeout(
|
|
595
|
-
(signal) => this.performDependencySync(signal),
|
|
596
|
-
|
|
679
|
+
(signal) => this.performDependencySync(sources, signal),
|
|
680
|
+
timeoutMs,
|
|
597
681
|
"Technology sync timed out"
|
|
598
682
|
);
|
|
599
683
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
@@ -601,25 +685,100 @@ var MonitorClient = class {
|
|
|
601
685
|
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
602
686
|
}
|
|
603
687
|
}
|
|
604
|
-
|
|
688
|
+
/**
|
|
689
|
+
* How many sequential registry batches a version-fetch over this many packages
|
|
690
|
+
* will take, given the shared concurrency limit. Shared by fetchLatestVersions
|
|
691
|
+
* (which does the batching) and calculateSyncTimeoutMs (which budgets for it),
|
|
692
|
+
* so the two can't drift out of sync with each other.
|
|
693
|
+
*/
|
|
694
|
+
registryBatchCount(itemCount) {
|
|
695
|
+
return Math.ceil(itemCount / CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Estimate a timeout budget that scales with how many roughly-equal-cost units of
|
|
699
|
+
* sequential work will run (registry batches, audit paths, ...), clamped to a
|
|
700
|
+
* floor/ceiling. Shared by calculateSyncTimeoutMs and calculateMultiPathAuditTimeoutMs
|
|
701
|
+
* so the two scaling formulas can't drift apart. At count=0 this always collapses to
|
|
702
|
+
* floorMs, since overheadMs alone never exceeds a floor sized for real work.
|
|
703
|
+
*/
|
|
704
|
+
estimateScaledTimeoutMs(count, perItemMs, overheadMs, floorMs, ceilingMs) {
|
|
705
|
+
const estimatedMs = count * perItemMs + overheadMs;
|
|
706
|
+
return Math.min(ceilingMs, Math.max(floorMs, estimatedMs));
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Scale the dependency sync timeout with how many registry lookups AND how many
|
|
710
|
+
* sequential POSTs (one per source, via sendTechnologiesWithEnvironment) it will
|
|
711
|
+
* make, so large dependency lists, many dependencySources, or a large requestTimeoutMs
|
|
712
|
+
* don't spuriously time out against a fixed budget.
|
|
713
|
+
*
|
|
714
|
+
* The POST-phase term (sources.length * requestTimeoutMs) is included even when
|
|
715
|
+
* version checking is off: performDependencySync still does one sequential POST per
|
|
716
|
+
* source regardless, so a flat overhead that ignores source count would under-budget
|
|
717
|
+
* that phase exactly the way a flat registry-batch estimate would under-budget large
|
|
718
|
+
* dependency lists.
|
|
719
|
+
*
|
|
720
|
+
* Registry batches are summed PER SOURCE (not over the combined total) because
|
|
721
|
+
* performDependencySync/fetchLatestVersions batch each source independently and
|
|
722
|
+
* sequentially — ceil(a/n) + ceil(b/n) can be larger than ceil((a+b)/n), so a
|
|
723
|
+
* single batch count over the combined total would under-estimate the real
|
|
724
|
+
* number of sequential registry round-trips when many small sources are configured.
|
|
725
|
+
*/
|
|
726
|
+
calculateSyncTimeoutMs(sources) {
|
|
727
|
+
const postPhaseMs = sources.length * this.requestTimeoutMs;
|
|
728
|
+
const batches = this.versionCheckEnabled ? sources.reduce((sum, source) => sum + this.registryBatchCount(source.technologies.length), 0) : 0;
|
|
729
|
+
return this.estimateScaledTimeoutMs(
|
|
730
|
+
batches,
|
|
731
|
+
this.registryTimeoutMs,
|
|
732
|
+
CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS + postPhaseMs,
|
|
733
|
+
CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
|
|
734
|
+
CONFIG_LIMITS.MAX_SYNC_DEPENDENCIES_TIMEOUT_MS
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Reads happen before syncDependencies' timeout/AbortSignal exists (see comment
|
|
739
|
+
* there), so this phase bounds itself with a wall-clock deadline on a budget that
|
|
740
|
+
* scales with how many sources are configured (mirroring calculateSyncTimeoutMs's
|
|
741
|
+
* scaling principle for the network phase). A plain deadline timestamp — not an
|
|
742
|
+
* AbortController — is enough here: readPackageJsonFromPath does purely synchronous,
|
|
743
|
+
* non-cancelable fs calls, so there is nothing for a signal to actually interrupt;
|
|
744
|
+
* it can only ever be checked between iterations, exactly like a deadline check.
|
|
745
|
+
* Once the budget is spent, stop reading further sources rather than always paying
|
|
746
|
+
* the full read cost for every configured dependencySource regardless of how long
|
|
747
|
+
* it's already taken — and report it via reportError, not just a console.warn, so a
|
|
748
|
+
* truncated sync isn't silently indistinguishable from a fully successful one on
|
|
749
|
+
* the dashboard.
|
|
750
|
+
*/
|
|
751
|
+
async loadDependencySources() {
|
|
605
752
|
if (this.dependencySources && this.dependencySources.length > 0) {
|
|
753
|
+
const sources = [];
|
|
754
|
+
const readBudgetMs = CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS * this.dependencySources.length;
|
|
755
|
+
const deadline = Date.now() + readBudgetMs;
|
|
606
756
|
for (const source of this.dependencySources) {
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
757
|
+
if (Date.now() >= deadline) {
|
|
758
|
+
const skippedCount = this.dependencySources.length - sources.length;
|
|
759
|
+
const warning = `Dependency source reads exceeded ${readBudgetMs}ms, skipping remaining ${skippedCount} source(s)`;
|
|
760
|
+
console.warn(`[MonitorClient] ${warning}`);
|
|
761
|
+
this.reportError("DEPENDENCY_SYNC", warning);
|
|
762
|
+
break;
|
|
610
763
|
}
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
|
|
614
|
-
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
615
|
-
console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
|
|
764
|
+
const technologies2 = await this.readPackageJsonFromPath(source.path);
|
|
765
|
+
sources.push({ environment: source.environment, technologies: technologies2 });
|
|
616
766
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
767
|
+
return sources;
|
|
768
|
+
}
|
|
769
|
+
const technologies = await this.readPackageJson();
|
|
770
|
+
return [{ environment: this.environment, technologies }];
|
|
771
|
+
}
|
|
772
|
+
async performDependencySync(sources, signal) {
|
|
773
|
+
for (const source of sources) {
|
|
774
|
+
if (signal?.aborted) {
|
|
775
|
+
console.log("[MonitorClient] Technology sync cancelled");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (source.technologies.length === 0) continue;
|
|
779
|
+
const enrichedTechnologies = await this.enrichWithLatestVersions(source.technologies, signal);
|
|
780
|
+
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
781
|
+
console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
|
|
623
782
|
}
|
|
624
783
|
}
|
|
625
784
|
/**
|
|
@@ -733,13 +892,22 @@ var MonitorClient = class {
|
|
|
733
892
|
try {
|
|
734
893
|
if (signal?.aborted) return null;
|
|
735
894
|
const encodedName = encodeURIComponent(packageName).replace("%40", "@");
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
895
|
+
const requestController = new AbortController();
|
|
896
|
+
const timeoutId = setTimeout(() => requestController.abort(), this.registryTimeoutMs);
|
|
897
|
+
const onOuterAbort = () => requestController.abort();
|
|
898
|
+
signal?.addEventListener("abort", onOuterAbort);
|
|
899
|
+
try {
|
|
900
|
+
const response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
|
|
901
|
+
headers: { "Accept": "application/json" },
|
|
902
|
+
signal: requestController.signal
|
|
903
|
+
});
|
|
904
|
+
if (!response.ok) return null;
|
|
905
|
+
const data = await response.json();
|
|
906
|
+
return data["dist-tags"]?.latest || null;
|
|
907
|
+
} finally {
|
|
908
|
+
clearTimeout(timeoutId);
|
|
909
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
910
|
+
}
|
|
743
911
|
} catch {
|
|
744
912
|
return null;
|
|
745
913
|
}
|
|
@@ -752,7 +920,7 @@ var MonitorClient = class {
|
|
|
752
920
|
async fetchLatestVersions(packageNames, signal) {
|
|
753
921
|
const results = /* @__PURE__ */ new Map();
|
|
754
922
|
const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
|
|
755
|
-
const totalBatches =
|
|
923
|
+
const totalBatches = this.registryBatchCount(packageNames.length);
|
|
756
924
|
for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
|
|
757
925
|
if (signal?.aborted) {
|
|
758
926
|
console.log("[MonitorClient] Version fetch cancelled");
|
|
@@ -923,7 +1091,7 @@ var MonitorClient = class {
|
|
|
923
1091
|
"child_process"
|
|
924
1092
|
);
|
|
925
1093
|
const { spawn } = childProcess;
|
|
926
|
-
return new Promise((resolve) => {
|
|
1094
|
+
return new Promise((resolve, reject) => {
|
|
927
1095
|
const proc = spawn(command, args, {
|
|
928
1096
|
cwd: options.cwd,
|
|
929
1097
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -932,10 +1100,8 @@ var MonitorClient = class {
|
|
|
932
1100
|
let stdout = "";
|
|
933
1101
|
let stderr = "";
|
|
934
1102
|
let timedOut = false;
|
|
935
|
-
let killed = false;
|
|
936
1103
|
const timeoutId = setTimeout(() => {
|
|
937
1104
|
timedOut = true;
|
|
938
|
-
killed = true;
|
|
939
1105
|
proc.kill("SIGTERM");
|
|
940
1106
|
setTimeout(() => {
|
|
941
1107
|
if (!proc.killed) {
|
|
@@ -955,14 +1121,38 @@ var MonitorClient = class {
|
|
|
955
1121
|
});
|
|
956
1122
|
proc.on("close", () => {
|
|
957
1123
|
clearTimeout(timeoutId);
|
|
958
|
-
resolve({ stdout, timedOut });
|
|
1124
|
+
resolve({ stdout, stderr, timedOut });
|
|
959
1125
|
});
|
|
960
|
-
proc.on("error", () => {
|
|
1126
|
+
proc.on("error", (err) => {
|
|
961
1127
|
clearTimeout(timeoutId);
|
|
962
|
-
|
|
1128
|
+
reject(err);
|
|
963
1129
|
});
|
|
964
1130
|
});
|
|
965
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* `npm audit --json` can exit without emitting valid JSON on stdout — e.g. the
|
|
1134
|
+
* registry is unreachable, or a fatal npm error is written to stderr instead.
|
|
1135
|
+
* Treat that as a soft failure (like a timeout) rather than throwing, so it
|
|
1136
|
+
* doesn't surface as a confusing "Unexpected end of JSON input" SDK_ERROR.
|
|
1137
|
+
*/
|
|
1138
|
+
parseNpmAuditJson(auditOutput, stderr) {
|
|
1139
|
+
const stderrSuffix = () => {
|
|
1140
|
+
const trimmedStderr = stderr.trim();
|
|
1141
|
+
return trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : "";
|
|
1142
|
+
};
|
|
1143
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1144
|
+
console.error(`[MonitorClient] npm audit produced no output${stderrSuffix()}`);
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
try {
|
|
1148
|
+
return JSON.parse(auditOutput);
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
console.error(
|
|
1151
|
+
`[MonitorClient] npm audit produced invalid JSON: ${err instanceof Error ? err.message : String(err)}${stderrSuffix()}`
|
|
1152
|
+
);
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
966
1156
|
/**
|
|
967
1157
|
* Run npm audit and send results to the monitoring server.
|
|
968
1158
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -1039,6 +1229,13 @@ var MonitorClient = class {
|
|
|
1039
1229
|
return null;
|
|
1040
1230
|
}
|
|
1041
1231
|
auditOutput = result2.stdout;
|
|
1232
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1233
|
+
const trimmedStderr = result2.stderr.trim();
|
|
1234
|
+
console.error(
|
|
1235
|
+
`[MonitorClient] yarn audit produced no output${trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : ""}`
|
|
1236
|
+
);
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1042
1239
|
vulnerabilities = this.parseYarnAuditOutput(auditOutput);
|
|
1043
1240
|
const lines = auditOutput.trim().split("\n");
|
|
1044
1241
|
for (const line of lines) {
|
|
@@ -1051,22 +1248,10 @@ var MonitorClient = class {
|
|
|
1051
1248
|
} catch {
|
|
1052
1249
|
}
|
|
1053
1250
|
}
|
|
1054
|
-
} else if (packageManager === "pnpm") {
|
|
1055
|
-
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1056
|
-
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1057
|
-
cwd: projectPath,
|
|
1058
|
-
timeout: this.auditTimeoutMs,
|
|
1059
|
-
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1060
|
-
});
|
|
1061
|
-
if (result2.timedOut) {
|
|
1062
|
-
console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
auditOutput = result2.stdout;
|
|
1066
|
-
const auditData = JSON.parse(auditOutput);
|
|
1067
|
-
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1068
|
-
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1069
1251
|
} else {
|
|
1252
|
+
if (packageManager === "pnpm") {
|
|
1253
|
+
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1254
|
+
}
|
|
1070
1255
|
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1071
1256
|
cwd: projectPath,
|
|
1072
1257
|
timeout: this.auditTimeoutMs,
|
|
@@ -1077,7 +1262,8 @@ var MonitorClient = class {
|
|
|
1077
1262
|
return null;
|
|
1078
1263
|
}
|
|
1079
1264
|
auditOutput = result2.stdout;
|
|
1080
|
-
const auditData =
|
|
1265
|
+
const auditData = this.parseNpmAuditJson(auditOutput, result2.stderr);
|
|
1266
|
+
if (!auditData) return null;
|
|
1081
1267
|
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1082
1268
|
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1083
1269
|
}
|
|
@@ -1122,9 +1308,10 @@ var MonitorClient = class {
|
|
|
1122
1308
|
}
|
|
1123
1309
|
console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
|
|
1124
1310
|
try {
|
|
1311
|
+
const timeoutMs = this.calculateMultiPathAuditTimeoutMs(this.auditPaths.length);
|
|
1125
1312
|
const result = await this.withTimeout(
|
|
1126
1313
|
() => this.performMultiPathAudit(),
|
|
1127
|
-
|
|
1314
|
+
timeoutMs,
|
|
1128
1315
|
"Multi-path audit timed out"
|
|
1129
1316
|
);
|
|
1130
1317
|
if (result) {
|
|
@@ -1136,6 +1323,21 @@ var MonitorClient = class {
|
|
|
1136
1323
|
return null;
|
|
1137
1324
|
}
|
|
1138
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Scale the multi-path audit timeout with how many paths it will scan, so
|
|
1328
|
+
* configuring many auditPaths (or a large per-path auditTimeoutMs) doesn't
|
|
1329
|
+
* spuriously time out the whole batch against a fixed budget — the same
|
|
1330
|
+
* failure mode calculateSyncTimeoutMs fixes for dependency sync.
|
|
1331
|
+
*/
|
|
1332
|
+
calculateMultiPathAuditTimeoutMs(pathCount) {
|
|
1333
|
+
return this.estimateScaledTimeoutMs(
|
|
1334
|
+
pathCount,
|
|
1335
|
+
this.auditTimeoutMs,
|
|
1336
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS,
|
|
1337
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
|
|
1338
|
+
CONFIG_LIMITS.MAX_AUDIT_MULTI_PATH_TIMEOUT_MS
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1139
1341
|
async performMultiPathAudit() {
|
|
1140
1342
|
if (!this.auditPaths) return null;
|
|
1141
1343
|
const results = [];
|
|
@@ -1558,7 +1760,7 @@ var MonitorClient = class {
|
|
|
1558
1760
|
}
|
|
1559
1761
|
try {
|
|
1560
1762
|
const hostname = os.hostname();
|
|
1561
|
-
const cpuPercent = await this.measureCpuPercent(os);
|
|
1763
|
+
const { percent: cpuPercent, model: cpuModel } = await this.measureCpuPercent(os);
|
|
1562
1764
|
const memoryTotal = os.totalmem();
|
|
1563
1765
|
const memoryAvailable = await this.getAvailableMemoryBytes(os, fs);
|
|
1564
1766
|
const memoryUsed = memoryTotal - memoryAvailable;
|
|
@@ -1567,6 +1769,7 @@ var MonitorClient = class {
|
|
|
1567
1769
|
await this.submitSystemMetric({
|
|
1568
1770
|
hostname,
|
|
1569
1771
|
cpuPercent,
|
|
1772
|
+
cpuModel,
|
|
1570
1773
|
memoryTotal,
|
|
1571
1774
|
memoryUsed,
|
|
1572
1775
|
memoryPercent,
|
|
@@ -1579,12 +1782,14 @@ var MonitorClient = class {
|
|
|
1579
1782
|
/**
|
|
1580
1783
|
* Measure CPU utilization by sampling cpus() twice with a 3s gap.
|
|
1581
1784
|
* The cumulative counters make this the average utilization over the
|
|
1582
|
-
* window, across all cores of the host. Returns a percentage (0–100)
|
|
1785
|
+
* window, across all cores of the host. Returns a percentage (0–100)
|
|
1786
|
+
* plus the CPU model, read off the same samples at zero extra cost.
|
|
1583
1787
|
*/
|
|
1584
1788
|
async measureCpuPercent(os) {
|
|
1585
1789
|
const sample1 = os.cpus();
|
|
1586
1790
|
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
1587
1791
|
const sample2 = os.cpus();
|
|
1792
|
+
const model = sample1[0]?.model ?? null;
|
|
1588
1793
|
let totalIdle = 0;
|
|
1589
1794
|
let totalTick = 0;
|
|
1590
1795
|
for (let i = 0; i < sample2.length; i++) {
|
|
@@ -1596,10 +1801,10 @@ var MonitorClient = class {
|
|
|
1596
1801
|
totalIdle += curr.times.idle - prev.times.idle;
|
|
1597
1802
|
}
|
|
1598
1803
|
if (totalTick <= 0) {
|
|
1599
|
-
return 0;
|
|
1804
|
+
return { percent: 0, model };
|
|
1600
1805
|
}
|
|
1601
1806
|
const idlePercent = totalIdle / totalTick * 100;
|
|
1602
|
-
return Math.max(0, Math.min(100, 100 - idlePercent));
|
|
1807
|
+
return { percent: Math.max(0, Math.min(100, 100 - idlePercent)), model };
|
|
1603
1808
|
}
|
|
1604
1809
|
/**
|
|
1605
1810
|
* Get reclaimable memory in bytes. On Linux, reads MemAvailable from
|
package/package.json
CHANGED