@ceon-oy/monitor-sdk 1.5.2 → 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 +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +225 -47
- package/dist/index.mjs +225 -47
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -261,6 +261,21 @@ declare class MonitorClient {
|
|
|
261
261
|
flush(): Promise<void>;
|
|
262
262
|
private getErrorKey;
|
|
263
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;
|
|
264
279
|
/**
|
|
265
280
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
266
281
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -325,6 +340,55 @@ declare class MonitorClient {
|
|
|
325
340
|
private startFlushTimer;
|
|
326
341
|
private stopFlushTimer;
|
|
327
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;
|
|
328
392
|
private performDependencySync;
|
|
329
393
|
/**
|
|
330
394
|
* Enrich technologies with latest version information from npm registry.
|
|
@@ -424,6 +488,13 @@ declare class MonitorClient {
|
|
|
424
488
|
* Only works in Node.js environment (not browser/bundled).
|
|
425
489
|
*/
|
|
426
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;
|
|
427
498
|
/**
|
|
428
499
|
* Run npm audit and send results to the monitoring server.
|
|
429
500
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -446,6 +517,13 @@ declare class MonitorClient {
|
|
|
446
517
|
* @returns Combined summary of all audit results
|
|
447
518
|
*/
|
|
448
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;
|
|
449
527
|
private performMultiPathAudit;
|
|
450
528
|
/**
|
|
451
529
|
* Parse npm audit JSON output into vulnerability items
|
package/dist/index.d.ts
CHANGED
|
@@ -261,6 +261,21 @@ declare class MonitorClient {
|
|
|
261
261
|
flush(): Promise<void>;
|
|
262
262
|
private getErrorKey;
|
|
263
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;
|
|
264
279
|
/**
|
|
265
280
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
266
281
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -325,6 +340,55 @@ declare class MonitorClient {
|
|
|
325
340
|
private startFlushTimer;
|
|
326
341
|
private stopFlushTimer;
|
|
327
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;
|
|
328
392
|
private performDependencySync;
|
|
329
393
|
/**
|
|
330
394
|
* Enrich technologies with latest version information from npm registry.
|
|
@@ -424,6 +488,13 @@ declare class MonitorClient {
|
|
|
424
488
|
* Only works in Node.js environment (not browser/bundled).
|
|
425
489
|
*/
|
|
426
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;
|
|
427
498
|
/**
|
|
428
499
|
* Run npm audit and send results to the monitoring server.
|
|
429
500
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -446,6 +517,13 @@ declare class MonitorClient {
|
|
|
446
517
|
* @returns Combined summary of all audit results
|
|
447
518
|
*/
|
|
448
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;
|
|
449
527
|
private performMultiPathAudit;
|
|
450
528
|
/**
|
|
451
529
|
* Parse npm audit JSON output into vulnerability items
|
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,
|
|
@@ -341,6 +349,46 @@ var MonitorClient = class {
|
|
|
341
349
|
await this.flushHealthResults();
|
|
342
350
|
await this.flushSdkErrors();
|
|
343
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
|
+
}
|
|
344
392
|
/**
|
|
345
393
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
346
394
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -349,8 +397,16 @@ var MonitorClient = class {
|
|
|
349
397
|
reportError(category, message, err) {
|
|
350
398
|
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
351
399
|
this.sdkErrorsInCurrentWindow++;
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
}
|
|
354
410
|
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
355
411
|
if (this.sdkErrorQueue.length >= 20) {
|
|
356
412
|
this.flushSdkErrors().catch(() => {
|
|
@@ -653,9 +709,11 @@ var MonitorClient = class {
|
|
|
653
709
|
async syncDependencies() {
|
|
654
710
|
console.log("[MonitorClient] Starting technology sync...");
|
|
655
711
|
try {
|
|
712
|
+
const sources = await this.loadDependencySources();
|
|
713
|
+
const timeoutMs = this.calculateSyncTimeoutMs(sources);
|
|
656
714
|
await this.withTimeout(
|
|
657
|
-
(signal) => this.performDependencySync(signal),
|
|
658
|
-
|
|
715
|
+
(signal) => this.performDependencySync(sources, signal),
|
|
716
|
+
timeoutMs,
|
|
659
717
|
"Technology sync timed out"
|
|
660
718
|
);
|
|
661
719
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
@@ -663,25 +721,100 @@ var MonitorClient = class {
|
|
|
663
721
|
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
664
722
|
}
|
|
665
723
|
}
|
|
666
|
-
|
|
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() {
|
|
667
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;
|
|
668
792
|
for (const source of this.dependencySources) {
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
|
|
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;
|
|
672
799
|
}
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
|
|
676
|
-
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
677
|
-
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 });
|
|
678
802
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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}`);
|
|
685
818
|
}
|
|
686
819
|
}
|
|
687
820
|
/**
|
|
@@ -795,13 +928,22 @@ var MonitorClient = class {
|
|
|
795
928
|
try {
|
|
796
929
|
if (signal?.aborted) return null;
|
|
797
930
|
const encodedName = encodeURIComponent(packageName).replace("%40", "@");
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
+
}
|
|
805
947
|
} catch {
|
|
806
948
|
return null;
|
|
807
949
|
}
|
|
@@ -814,7 +956,7 @@ var MonitorClient = class {
|
|
|
814
956
|
async fetchLatestVersions(packageNames, signal) {
|
|
815
957
|
const results = /* @__PURE__ */ new Map();
|
|
816
958
|
const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
|
|
817
|
-
const totalBatches =
|
|
959
|
+
const totalBatches = this.registryBatchCount(packageNames.length);
|
|
818
960
|
for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
|
|
819
961
|
if (signal?.aborted) {
|
|
820
962
|
console.log("[MonitorClient] Version fetch cancelled");
|
|
@@ -1015,7 +1157,7 @@ var MonitorClient = class {
|
|
|
1015
1157
|
});
|
|
1016
1158
|
proc.on("close", () => {
|
|
1017
1159
|
clearTimeout(timeoutId);
|
|
1018
|
-
resolve({ stdout, timedOut });
|
|
1160
|
+
resolve({ stdout, stderr, timedOut });
|
|
1019
1161
|
});
|
|
1020
1162
|
proc.on("error", (err) => {
|
|
1021
1163
|
clearTimeout(timeoutId);
|
|
@@ -1023,6 +1165,30 @@ var MonitorClient = class {
|
|
|
1023
1165
|
});
|
|
1024
1166
|
});
|
|
1025
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
|
+
}
|
|
1026
1192
|
/**
|
|
1027
1193
|
* Run npm audit and send results to the monitoring server.
|
|
1028
1194
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -1099,6 +1265,13 @@ var MonitorClient = class {
|
|
|
1099
1265
|
return null;
|
|
1100
1266
|
}
|
|
1101
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
|
+
}
|
|
1102
1275
|
vulnerabilities = this.parseYarnAuditOutput(auditOutput);
|
|
1103
1276
|
const lines = auditOutput.trim().split("\n");
|
|
1104
1277
|
for (const line of lines) {
|
|
@@ -1111,22 +1284,10 @@ var MonitorClient = class {
|
|
|
1111
1284
|
} catch {
|
|
1112
1285
|
}
|
|
1113
1286
|
}
|
|
1114
|
-
} else if (packageManager === "pnpm") {
|
|
1115
|
-
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1116
|
-
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1117
|
-
cwd: projectPath,
|
|
1118
|
-
timeout: this.auditTimeoutMs,
|
|
1119
|
-
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1120
|
-
});
|
|
1121
|
-
if (result2.timedOut) {
|
|
1122
|
-
console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
|
|
1123
|
-
return null;
|
|
1124
|
-
}
|
|
1125
|
-
auditOutput = result2.stdout;
|
|
1126
|
-
const auditData = JSON.parse(auditOutput);
|
|
1127
|
-
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1128
|
-
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1129
1287
|
} else {
|
|
1288
|
+
if (packageManager === "pnpm") {
|
|
1289
|
+
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1290
|
+
}
|
|
1130
1291
|
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1131
1292
|
cwd: projectPath,
|
|
1132
1293
|
timeout: this.auditTimeoutMs,
|
|
@@ -1137,7 +1298,8 @@ var MonitorClient = class {
|
|
|
1137
1298
|
return null;
|
|
1138
1299
|
}
|
|
1139
1300
|
auditOutput = result2.stdout;
|
|
1140
|
-
const auditData =
|
|
1301
|
+
const auditData = this.parseNpmAuditJson(auditOutput, result2.stderr);
|
|
1302
|
+
if (!auditData) return null;
|
|
1141
1303
|
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1142
1304
|
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1143
1305
|
}
|
|
@@ -1182,9 +1344,10 @@ var MonitorClient = class {
|
|
|
1182
1344
|
}
|
|
1183
1345
|
console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
|
|
1184
1346
|
try {
|
|
1347
|
+
const timeoutMs = this.calculateMultiPathAuditTimeoutMs(this.auditPaths.length);
|
|
1185
1348
|
const result = await this.withTimeout(
|
|
1186
1349
|
() => this.performMultiPathAudit(),
|
|
1187
|
-
|
|
1350
|
+
timeoutMs,
|
|
1188
1351
|
"Multi-path audit timed out"
|
|
1189
1352
|
);
|
|
1190
1353
|
if (result) {
|
|
@@ -1196,6 +1359,21 @@ var MonitorClient = class {
|
|
|
1196
1359
|
return null;
|
|
1197
1360
|
}
|
|
1198
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
|
+
}
|
|
1199
1377
|
async performMultiPathAudit() {
|
|
1200
1378
|
if (!this.auditPaths) return null;
|
|
1201
1379
|
const results = [];
|
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,
|
|
@@ -305,6 +313,46 @@ var MonitorClient = class {
|
|
|
305
313
|
await this.flushHealthResults();
|
|
306
314
|
await this.flushSdkErrors();
|
|
307
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
|
+
}
|
|
308
356
|
/**
|
|
309
357
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
310
358
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -313,8 +361,16 @@ var MonitorClient = class {
|
|
|
313
361
|
reportError(category, message, err) {
|
|
314
362
|
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
315
363
|
this.sdkErrorsInCurrentWindow++;
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}
|
|
318
374
|
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
319
375
|
if (this.sdkErrorQueue.length >= 20) {
|
|
320
376
|
this.flushSdkErrors().catch(() => {
|
|
@@ -617,9 +673,11 @@ var MonitorClient = class {
|
|
|
617
673
|
async syncDependencies() {
|
|
618
674
|
console.log("[MonitorClient] Starting technology sync...");
|
|
619
675
|
try {
|
|
676
|
+
const sources = await this.loadDependencySources();
|
|
677
|
+
const timeoutMs = this.calculateSyncTimeoutMs(sources);
|
|
620
678
|
await this.withTimeout(
|
|
621
|
-
(signal) => this.performDependencySync(signal),
|
|
622
|
-
|
|
679
|
+
(signal) => this.performDependencySync(sources, signal),
|
|
680
|
+
timeoutMs,
|
|
623
681
|
"Technology sync timed out"
|
|
624
682
|
);
|
|
625
683
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
@@ -627,25 +685,100 @@ var MonitorClient = class {
|
|
|
627
685
|
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
628
686
|
}
|
|
629
687
|
}
|
|
630
|
-
|
|
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() {
|
|
631
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;
|
|
632
756
|
for (const source of this.dependencySources) {
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
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;
|
|
636
763
|
}
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
|
|
640
|
-
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
641
|
-
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 });
|
|
642
766
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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}`);
|
|
649
782
|
}
|
|
650
783
|
}
|
|
651
784
|
/**
|
|
@@ -759,13 +892,22 @@ var MonitorClient = class {
|
|
|
759
892
|
try {
|
|
760
893
|
if (signal?.aborted) return null;
|
|
761
894
|
const encodedName = encodeURIComponent(packageName).replace("%40", "@");
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
+
}
|
|
769
911
|
} catch {
|
|
770
912
|
return null;
|
|
771
913
|
}
|
|
@@ -778,7 +920,7 @@ var MonitorClient = class {
|
|
|
778
920
|
async fetchLatestVersions(packageNames, signal) {
|
|
779
921
|
const results = /* @__PURE__ */ new Map();
|
|
780
922
|
const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
|
|
781
|
-
const totalBatches =
|
|
923
|
+
const totalBatches = this.registryBatchCount(packageNames.length);
|
|
782
924
|
for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
|
|
783
925
|
if (signal?.aborted) {
|
|
784
926
|
console.log("[MonitorClient] Version fetch cancelled");
|
|
@@ -979,7 +1121,7 @@ var MonitorClient = class {
|
|
|
979
1121
|
});
|
|
980
1122
|
proc.on("close", () => {
|
|
981
1123
|
clearTimeout(timeoutId);
|
|
982
|
-
resolve({ stdout, timedOut });
|
|
1124
|
+
resolve({ stdout, stderr, timedOut });
|
|
983
1125
|
});
|
|
984
1126
|
proc.on("error", (err) => {
|
|
985
1127
|
clearTimeout(timeoutId);
|
|
@@ -987,6 +1129,30 @@ var MonitorClient = class {
|
|
|
987
1129
|
});
|
|
988
1130
|
});
|
|
989
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
|
+
}
|
|
990
1156
|
/**
|
|
991
1157
|
* Run npm audit and send results to the monitoring server.
|
|
992
1158
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -1063,6 +1229,13 @@ var MonitorClient = class {
|
|
|
1063
1229
|
return null;
|
|
1064
1230
|
}
|
|
1065
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
|
+
}
|
|
1066
1239
|
vulnerabilities = this.parseYarnAuditOutput(auditOutput);
|
|
1067
1240
|
const lines = auditOutput.trim().split("\n");
|
|
1068
1241
|
for (const line of lines) {
|
|
@@ -1075,22 +1248,10 @@ var MonitorClient = class {
|
|
|
1075
1248
|
} catch {
|
|
1076
1249
|
}
|
|
1077
1250
|
}
|
|
1078
|
-
} else if (packageManager === "pnpm") {
|
|
1079
|
-
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1080
|
-
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1081
|
-
cwd: projectPath,
|
|
1082
|
-
timeout: this.auditTimeoutMs,
|
|
1083
|
-
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1084
|
-
});
|
|
1085
|
-
if (result2.timedOut) {
|
|
1086
|
-
console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
|
|
1087
|
-
return null;
|
|
1088
|
-
}
|
|
1089
|
-
auditOutput = result2.stdout;
|
|
1090
|
-
const auditData = JSON.parse(auditOutput);
|
|
1091
|
-
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1092
|
-
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1093
1251
|
} else {
|
|
1252
|
+
if (packageManager === "pnpm") {
|
|
1253
|
+
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1254
|
+
}
|
|
1094
1255
|
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1095
1256
|
cwd: projectPath,
|
|
1096
1257
|
timeout: this.auditTimeoutMs,
|
|
@@ -1101,7 +1262,8 @@ var MonitorClient = class {
|
|
|
1101
1262
|
return null;
|
|
1102
1263
|
}
|
|
1103
1264
|
auditOutput = result2.stdout;
|
|
1104
|
-
const auditData =
|
|
1265
|
+
const auditData = this.parseNpmAuditJson(auditOutput, result2.stderr);
|
|
1266
|
+
if (!auditData) return null;
|
|
1105
1267
|
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1106
1268
|
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1107
1269
|
}
|
|
@@ -1146,9 +1308,10 @@ var MonitorClient = class {
|
|
|
1146
1308
|
}
|
|
1147
1309
|
console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
|
|
1148
1310
|
try {
|
|
1311
|
+
const timeoutMs = this.calculateMultiPathAuditTimeoutMs(this.auditPaths.length);
|
|
1149
1312
|
const result = await this.withTimeout(
|
|
1150
1313
|
() => this.performMultiPathAudit(),
|
|
1151
|
-
|
|
1314
|
+
timeoutMs,
|
|
1152
1315
|
"Multi-path audit timed out"
|
|
1153
1316
|
);
|
|
1154
1317
|
if (result) {
|
|
@@ -1160,6 +1323,21 @@ var MonitorClient = class {
|
|
|
1160
1323
|
return null;
|
|
1161
1324
|
}
|
|
1162
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
|
+
}
|
|
1163
1341
|
async performMultiPathAudit() {
|
|
1164
1342
|
if (!this.auditPaths) return null;
|
|
1165
1343
|
const results = [];
|
package/package.json
CHANGED