@ceon-oy/monitor-sdk 1.5.2 → 1.5.5
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 +234 -51
- package/dist/index.mjs +234 -51
- 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
|
@@ -53,8 +53,9 @@ var CONFIG_LIMITS = {
|
|
|
53
53
|
// 10MB
|
|
54
54
|
AUDIT_TIMEOUT_MS: 6e4,
|
|
55
55
|
// 60 seconds (default)
|
|
56
|
-
MAX_AUDIT_TIMEOUT_MS:
|
|
57
|
-
//
|
|
56
|
+
MAX_AUDIT_TIMEOUT_MS: 18e5,
|
|
57
|
+
// 30 minutes (max configurable) - was 5 minutes, which every
|
|
58
|
+
// real npm audit run on Cloud Run (slow registry egress) exceeded, silently returning null
|
|
58
59
|
SETTINGS_POLL_INTERVAL_MS: 5 * 60 * 1e3,
|
|
59
60
|
// 5 minutes
|
|
60
61
|
REGISTRY_TIMEOUT_MS: 5e3,
|
|
@@ -64,9 +65,17 @@ var CONFIG_LIMITS = {
|
|
|
64
65
|
TECH_VERSION_FETCH_TIMEOUT_MS: 3e4,
|
|
65
66
|
// 30 seconds max for all version fetches
|
|
66
67
|
SYNC_DEPENDENCIES_TIMEOUT_MS: 6e4,
|
|
67
|
-
//
|
|
68
|
+
// floor: minimum timeout for all dependency syncs
|
|
69
|
+
MAX_SYNC_DEPENDENCIES_TIMEOUT_MS: 3e5,
|
|
70
|
+
// ceiling: 5 minutes, even for very large dependency lists
|
|
71
|
+
SYNC_FIXED_OVERHEAD_MS: 1e4,
|
|
72
|
+
// buffer per sync for local reads + POSTs to the server
|
|
68
73
|
AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
|
|
69
|
-
//
|
|
74
|
+
// floor: minimum timeout for all audit paths
|
|
75
|
+
MAX_AUDIT_MULTI_PATH_TIMEOUT_MS: 18e5,
|
|
76
|
+
// ceiling: 30 minutes, even for many audit paths
|
|
77
|
+
AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS: 1e4,
|
|
78
|
+
// buffer per multi-path audit for local reads + POSTs to the server
|
|
70
79
|
REGISTRY_CONCURRENCY_LIMIT: 5,
|
|
71
80
|
// Limit parallel requests to avoid rate limiting
|
|
72
81
|
HEALTH_CHECK_FETCH_INTERVAL_MS: 6e4,
|
|
@@ -341,6 +350,46 @@ var MonitorClient = class {
|
|
|
341
350
|
await this.flushHealthResults();
|
|
342
351
|
await this.flushSdkErrors();
|
|
343
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Node's fetch() (undici) wraps network-level failures (ECONNREFUSED, DNS lookup
|
|
355
|
+
* failures, TLS errors, ...) in a generic `TypeError: fetch failed`, with the real
|
|
356
|
+
* reason attached via the standard `cause` chain. Without unwrapping it, every
|
|
357
|
+
* network failure across the SDK is reported as the same opaque "fetch failed"
|
|
358
|
+
* message, with no way to diagnose it from the dashboard.
|
|
359
|
+
*/
|
|
360
|
+
describeErrorCause(err, depth = 0) {
|
|
361
|
+
if (depth > 3) return void 0;
|
|
362
|
+
const cause = err.cause;
|
|
363
|
+
if (cause === void 0 || cause === null) return void 0;
|
|
364
|
+
const aggregateErrors = cause.errors;
|
|
365
|
+
if (Array.isArray(aggregateErrors)) {
|
|
366
|
+
const childSummary = aggregateErrors.map((e) => e instanceof Error ? e.message : String(e)).join("; ");
|
|
367
|
+
const summary = cause instanceof Error && cause.message ? `${cause.message} (${childSummary})` : childSummary;
|
|
368
|
+
return this.truncateDescription(summary);
|
|
369
|
+
}
|
|
370
|
+
if (cause instanceof Error) {
|
|
371
|
+
const nested = this.describeErrorCause(cause, depth + 1);
|
|
372
|
+
const description = nested ? `${cause.message} (${nested})` : cause.message;
|
|
373
|
+
return this.truncateDescription(description);
|
|
374
|
+
}
|
|
375
|
+
return this.truncateDescription(String(cause));
|
|
376
|
+
}
|
|
377
|
+
truncateDescription(description) {
|
|
378
|
+
const limit = 500;
|
|
379
|
+
return description.length > limit ? `${description.slice(0, limit)}...` : description;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Run `fn`, swallowing any throw and returning undefined instead. Used to read
|
|
383
|
+
* possibly-hostile Error fields (a throwing message/stack/cause getter) without
|
|
384
|
+
* losing whatever else was already read — each call degrades independently.
|
|
385
|
+
*/
|
|
386
|
+
safe(fn) {
|
|
387
|
+
try {
|
|
388
|
+
return fn();
|
|
389
|
+
} catch {
|
|
390
|
+
return void 0;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
344
393
|
/**
|
|
345
394
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
346
395
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -349,8 +398,16 @@ var MonitorClient = class {
|
|
|
349
398
|
reportError(category, message, err) {
|
|
350
399
|
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
351
400
|
this.sdkErrorsInCurrentWindow++;
|
|
352
|
-
|
|
353
|
-
|
|
401
|
+
let errorMessage = message;
|
|
402
|
+
let stack;
|
|
403
|
+
if (err instanceof Error) {
|
|
404
|
+
errorMessage = this.safe(() => `${message}: ${err.message}`) ?? message;
|
|
405
|
+
const causeDescription = this.safe(() => this.describeErrorCause(err));
|
|
406
|
+
if (causeDescription) {
|
|
407
|
+
errorMessage += ` (cause: ${causeDescription})`;
|
|
408
|
+
}
|
|
409
|
+
stack = this.safe(() => err.stack);
|
|
410
|
+
}
|
|
354
411
|
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
355
412
|
if (this.sdkErrorQueue.length >= 20) {
|
|
356
413
|
this.flushSdkErrors().catch(() => {
|
|
@@ -653,9 +710,11 @@ var MonitorClient = class {
|
|
|
653
710
|
async syncDependencies() {
|
|
654
711
|
console.log("[MonitorClient] Starting technology sync...");
|
|
655
712
|
try {
|
|
713
|
+
const sources = await this.loadDependencySources();
|
|
714
|
+
const timeoutMs = this.calculateSyncTimeoutMs(sources);
|
|
656
715
|
await this.withTimeout(
|
|
657
|
-
(signal) => this.performDependencySync(signal),
|
|
658
|
-
|
|
716
|
+
(signal) => this.performDependencySync(sources, signal),
|
|
717
|
+
timeoutMs,
|
|
659
718
|
"Technology sync timed out"
|
|
660
719
|
);
|
|
661
720
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
@@ -663,25 +722,100 @@ var MonitorClient = class {
|
|
|
663
722
|
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
664
723
|
}
|
|
665
724
|
}
|
|
666
|
-
|
|
725
|
+
/**
|
|
726
|
+
* How many sequential registry batches a version-fetch over this many packages
|
|
727
|
+
* will take, given the shared concurrency limit. Shared by fetchLatestVersions
|
|
728
|
+
* (which does the batching) and calculateSyncTimeoutMs (which budgets for it),
|
|
729
|
+
* so the two can't drift out of sync with each other.
|
|
730
|
+
*/
|
|
731
|
+
registryBatchCount(itemCount) {
|
|
732
|
+
return Math.ceil(itemCount / CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Estimate a timeout budget that scales with how many roughly-equal-cost units of
|
|
736
|
+
* sequential work will run (registry batches, audit paths, ...), clamped to a
|
|
737
|
+
* floor/ceiling. Shared by calculateSyncTimeoutMs and calculateMultiPathAuditTimeoutMs
|
|
738
|
+
* so the two scaling formulas can't drift apart. At count=0 this always collapses to
|
|
739
|
+
* floorMs, since overheadMs alone never exceeds a floor sized for real work.
|
|
740
|
+
*/
|
|
741
|
+
estimateScaledTimeoutMs(count, perItemMs, overheadMs, floorMs, ceilingMs) {
|
|
742
|
+
const estimatedMs = count * perItemMs + overheadMs;
|
|
743
|
+
return Math.min(ceilingMs, Math.max(floorMs, estimatedMs));
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Scale the dependency sync timeout with how many registry lookups AND how many
|
|
747
|
+
* sequential POSTs (one per source, via sendTechnologiesWithEnvironment) it will
|
|
748
|
+
* make, so large dependency lists, many dependencySources, or a large requestTimeoutMs
|
|
749
|
+
* don't spuriously time out against a fixed budget.
|
|
750
|
+
*
|
|
751
|
+
* The POST-phase term (sources.length * requestTimeoutMs) is included even when
|
|
752
|
+
* version checking is off: performDependencySync still does one sequential POST per
|
|
753
|
+
* source regardless, so a flat overhead that ignores source count would under-budget
|
|
754
|
+
* that phase exactly the way a flat registry-batch estimate would under-budget large
|
|
755
|
+
* dependency lists.
|
|
756
|
+
*
|
|
757
|
+
* Registry batches are summed PER SOURCE (not over the combined total) because
|
|
758
|
+
* performDependencySync/fetchLatestVersions batch each source independently and
|
|
759
|
+
* sequentially — ceil(a/n) + ceil(b/n) can be larger than ceil((a+b)/n), so a
|
|
760
|
+
* single batch count over the combined total would under-estimate the real
|
|
761
|
+
* number of sequential registry round-trips when many small sources are configured.
|
|
762
|
+
*/
|
|
763
|
+
calculateSyncTimeoutMs(sources) {
|
|
764
|
+
const postPhaseMs = sources.length * this.requestTimeoutMs;
|
|
765
|
+
const batches = this.versionCheckEnabled ? sources.reduce((sum, source) => sum + this.registryBatchCount(source.technologies.length), 0) : 0;
|
|
766
|
+
return this.estimateScaledTimeoutMs(
|
|
767
|
+
batches,
|
|
768
|
+
this.registryTimeoutMs,
|
|
769
|
+
CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS + postPhaseMs,
|
|
770
|
+
CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
|
|
771
|
+
CONFIG_LIMITS.MAX_SYNC_DEPENDENCIES_TIMEOUT_MS
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Reads happen before syncDependencies' timeout/AbortSignal exists (see comment
|
|
776
|
+
* there), so this phase bounds itself with a wall-clock deadline on a budget that
|
|
777
|
+
* scales with how many sources are configured (mirroring calculateSyncTimeoutMs's
|
|
778
|
+
* scaling principle for the network phase). A plain deadline timestamp — not an
|
|
779
|
+
* AbortController — is enough here: readPackageJsonFromPath does purely synchronous,
|
|
780
|
+
* non-cancelable fs calls, so there is nothing for a signal to actually interrupt;
|
|
781
|
+
* it can only ever be checked between iterations, exactly like a deadline check.
|
|
782
|
+
* Once the budget is spent, stop reading further sources rather than always paying
|
|
783
|
+
* the full read cost for every configured dependencySource regardless of how long
|
|
784
|
+
* it's already taken — and report it via reportError, not just a console.warn, so a
|
|
785
|
+
* truncated sync isn't silently indistinguishable from a fully successful one on
|
|
786
|
+
* the dashboard.
|
|
787
|
+
*/
|
|
788
|
+
async loadDependencySources() {
|
|
667
789
|
if (this.dependencySources && this.dependencySources.length > 0) {
|
|
790
|
+
const sources = [];
|
|
791
|
+
const readBudgetMs = CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS * this.dependencySources.length;
|
|
792
|
+
const deadline = Date.now() + readBudgetMs;
|
|
668
793
|
for (const source of this.dependencySources) {
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
|
|
794
|
+
if (Date.now() >= deadline) {
|
|
795
|
+
const skippedCount = this.dependencySources.length - sources.length;
|
|
796
|
+
const warning = `Dependency source reads exceeded ${readBudgetMs}ms, skipping remaining ${skippedCount} source(s)`;
|
|
797
|
+
console.warn(`[MonitorClient] ${warning}`);
|
|
798
|
+
this.reportError("DEPENDENCY_SYNC", warning);
|
|
799
|
+
break;
|
|
672
800
|
}
|
|
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}`);
|
|
801
|
+
const technologies2 = await this.readPackageJsonFromPath(source.path);
|
|
802
|
+
sources.push({ environment: source.environment, technologies: technologies2 });
|
|
678
803
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
804
|
+
return sources;
|
|
805
|
+
}
|
|
806
|
+
const technologies = await this.readPackageJson();
|
|
807
|
+
return [{ environment: this.environment, technologies }];
|
|
808
|
+
}
|
|
809
|
+
async performDependencySync(sources, signal) {
|
|
810
|
+
for (const source of sources) {
|
|
811
|
+
if (signal?.aborted) {
|
|
812
|
+
console.log("[MonitorClient] Technology sync cancelled");
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (source.technologies.length === 0) continue;
|
|
816
|
+
const enrichedTechnologies = await this.enrichWithLatestVersions(source.technologies, signal);
|
|
817
|
+
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
818
|
+
console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
|
|
685
819
|
}
|
|
686
820
|
}
|
|
687
821
|
/**
|
|
@@ -795,13 +929,22 @@ var MonitorClient = class {
|
|
|
795
929
|
try {
|
|
796
930
|
if (signal?.aborted) return null;
|
|
797
931
|
const encodedName = encodeURIComponent(packageName).replace("%40", "@");
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
932
|
+
const requestController = new AbortController();
|
|
933
|
+
const timeoutId = setTimeout(() => requestController.abort(), this.registryTimeoutMs);
|
|
934
|
+
const onOuterAbort = () => requestController.abort();
|
|
935
|
+
signal?.addEventListener("abort", onOuterAbort);
|
|
936
|
+
try {
|
|
937
|
+
const response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
|
|
938
|
+
headers: { "Accept": "application/json" },
|
|
939
|
+
signal: requestController.signal
|
|
940
|
+
});
|
|
941
|
+
if (!response.ok) return null;
|
|
942
|
+
const data = await response.json();
|
|
943
|
+
return data["dist-tags"]?.latest || null;
|
|
944
|
+
} finally {
|
|
945
|
+
clearTimeout(timeoutId);
|
|
946
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
947
|
+
}
|
|
805
948
|
} catch {
|
|
806
949
|
return null;
|
|
807
950
|
}
|
|
@@ -814,7 +957,7 @@ var MonitorClient = class {
|
|
|
814
957
|
async fetchLatestVersions(packageNames, signal) {
|
|
815
958
|
const results = /* @__PURE__ */ new Map();
|
|
816
959
|
const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
|
|
817
|
-
const totalBatches =
|
|
960
|
+
const totalBatches = this.registryBatchCount(packageNames.length);
|
|
818
961
|
for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
|
|
819
962
|
if (signal?.aborted) {
|
|
820
963
|
console.log("[MonitorClient] Version fetch cancelled");
|
|
@@ -1015,7 +1158,7 @@ var MonitorClient = class {
|
|
|
1015
1158
|
});
|
|
1016
1159
|
proc.on("close", () => {
|
|
1017
1160
|
clearTimeout(timeoutId);
|
|
1018
|
-
resolve({ stdout, timedOut });
|
|
1161
|
+
resolve({ stdout, stderr, timedOut });
|
|
1019
1162
|
});
|
|
1020
1163
|
proc.on("error", (err) => {
|
|
1021
1164
|
clearTimeout(timeoutId);
|
|
@@ -1023,6 +1166,30 @@ var MonitorClient = class {
|
|
|
1023
1166
|
});
|
|
1024
1167
|
});
|
|
1025
1168
|
}
|
|
1169
|
+
/**
|
|
1170
|
+
* `npm audit --json` can exit without emitting valid JSON on stdout — e.g. the
|
|
1171
|
+
* registry is unreachable, or a fatal npm error is written to stderr instead.
|
|
1172
|
+
* Treat that as a soft failure (like a timeout) rather than throwing, so it
|
|
1173
|
+
* doesn't surface as a confusing "Unexpected end of JSON input" SDK_ERROR.
|
|
1174
|
+
*/
|
|
1175
|
+
parseNpmAuditJson(auditOutput, stderr) {
|
|
1176
|
+
const stderrSuffix = () => {
|
|
1177
|
+
const trimmedStderr = stderr.trim();
|
|
1178
|
+
return trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : "";
|
|
1179
|
+
};
|
|
1180
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1181
|
+
console.error(`[MonitorClient] npm audit produced no output${stderrSuffix()}`);
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
try {
|
|
1185
|
+
return JSON.parse(auditOutput);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
console.error(
|
|
1188
|
+
`[MonitorClient] npm audit produced invalid JSON: ${err instanceof Error ? err.message : String(err)}${stderrSuffix()}`
|
|
1189
|
+
);
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1026
1193
|
/**
|
|
1027
1194
|
* Run npm audit and send results to the monitoring server.
|
|
1028
1195
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -1095,10 +1262,19 @@ var MonitorClient = class {
|
|
|
1095
1262
|
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1096
1263
|
});
|
|
1097
1264
|
if (result2.timedOut) {
|
|
1098
|
-
|
|
1265
|
+
const message = `yarn audit timed out after ${this.auditTimeoutMs}ms`;
|
|
1266
|
+
console.error(`[MonitorClient] ${message}`);
|
|
1267
|
+
this.reportError("VULNERABILITY_SCAN", message);
|
|
1099
1268
|
return null;
|
|
1100
1269
|
}
|
|
1101
1270
|
auditOutput = result2.stdout;
|
|
1271
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1272
|
+
const trimmedStderr = result2.stderr.trim();
|
|
1273
|
+
console.error(
|
|
1274
|
+
`[MonitorClient] yarn audit produced no output${trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : ""}`
|
|
1275
|
+
);
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1102
1278
|
vulnerabilities = this.parseYarnAuditOutput(auditOutput);
|
|
1103
1279
|
const lines = auditOutput.trim().split("\n");
|
|
1104
1280
|
for (const line of lines) {
|
|
@@ -1111,33 +1287,24 @@ var MonitorClient = class {
|
|
|
1111
1287
|
} catch {
|
|
1112
1288
|
}
|
|
1113
1289
|
}
|
|
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
1290
|
} else {
|
|
1291
|
+
if (packageManager === "pnpm") {
|
|
1292
|
+
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1293
|
+
}
|
|
1130
1294
|
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1131
1295
|
cwd: projectPath,
|
|
1132
1296
|
timeout: this.auditTimeoutMs,
|
|
1133
1297
|
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1134
1298
|
});
|
|
1135
1299
|
if (result2.timedOut) {
|
|
1136
|
-
|
|
1300
|
+
const message = `npm audit timed out after ${this.auditTimeoutMs}ms`;
|
|
1301
|
+
console.error(`[MonitorClient] ${message}`);
|
|
1302
|
+
this.reportError("VULNERABILITY_SCAN", message);
|
|
1137
1303
|
return null;
|
|
1138
1304
|
}
|
|
1139
1305
|
auditOutput = result2.stdout;
|
|
1140
|
-
const auditData =
|
|
1306
|
+
const auditData = this.parseNpmAuditJson(auditOutput, result2.stderr);
|
|
1307
|
+
if (!auditData) return null;
|
|
1141
1308
|
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1142
1309
|
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1143
1310
|
}
|
|
@@ -1182,9 +1349,10 @@ var MonitorClient = class {
|
|
|
1182
1349
|
}
|
|
1183
1350
|
console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
|
|
1184
1351
|
try {
|
|
1352
|
+
const timeoutMs = this.calculateMultiPathAuditTimeoutMs(this.auditPaths.length);
|
|
1185
1353
|
const result = await this.withTimeout(
|
|
1186
1354
|
() => this.performMultiPathAudit(),
|
|
1187
|
-
|
|
1355
|
+
timeoutMs,
|
|
1188
1356
|
"Multi-path audit timed out"
|
|
1189
1357
|
);
|
|
1190
1358
|
if (result) {
|
|
@@ -1196,6 +1364,21 @@ var MonitorClient = class {
|
|
|
1196
1364
|
return null;
|
|
1197
1365
|
}
|
|
1198
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Scale the multi-path audit timeout with how many paths it will scan, so
|
|
1369
|
+
* configuring many auditPaths (or a large per-path auditTimeoutMs) doesn't
|
|
1370
|
+
* spuriously time out the whole batch against a fixed budget — the same
|
|
1371
|
+
* failure mode calculateSyncTimeoutMs fixes for dependency sync.
|
|
1372
|
+
*/
|
|
1373
|
+
calculateMultiPathAuditTimeoutMs(pathCount) {
|
|
1374
|
+
return this.estimateScaledTimeoutMs(
|
|
1375
|
+
pathCount,
|
|
1376
|
+
this.auditTimeoutMs,
|
|
1377
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS,
|
|
1378
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
|
|
1379
|
+
CONFIG_LIMITS.MAX_AUDIT_MULTI_PATH_TIMEOUT_MS
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1199
1382
|
async performMultiPathAudit() {
|
|
1200
1383
|
if (!this.auditPaths) return null;
|
|
1201
1384
|
const results = [];
|
package/dist/index.mjs
CHANGED
|
@@ -17,8 +17,9 @@ var CONFIG_LIMITS = {
|
|
|
17
17
|
// 10MB
|
|
18
18
|
AUDIT_TIMEOUT_MS: 6e4,
|
|
19
19
|
// 60 seconds (default)
|
|
20
|
-
MAX_AUDIT_TIMEOUT_MS:
|
|
21
|
-
//
|
|
20
|
+
MAX_AUDIT_TIMEOUT_MS: 18e5,
|
|
21
|
+
// 30 minutes (max configurable) - was 5 minutes, which every
|
|
22
|
+
// real npm audit run on Cloud Run (slow registry egress) exceeded, silently returning null
|
|
22
23
|
SETTINGS_POLL_INTERVAL_MS: 5 * 60 * 1e3,
|
|
23
24
|
// 5 minutes
|
|
24
25
|
REGISTRY_TIMEOUT_MS: 5e3,
|
|
@@ -28,9 +29,17 @@ var CONFIG_LIMITS = {
|
|
|
28
29
|
TECH_VERSION_FETCH_TIMEOUT_MS: 3e4,
|
|
29
30
|
// 30 seconds max for all version fetches
|
|
30
31
|
SYNC_DEPENDENCIES_TIMEOUT_MS: 6e4,
|
|
31
|
-
//
|
|
32
|
+
// floor: minimum timeout for all dependency syncs
|
|
33
|
+
MAX_SYNC_DEPENDENCIES_TIMEOUT_MS: 3e5,
|
|
34
|
+
// ceiling: 5 minutes, even for very large dependency lists
|
|
35
|
+
SYNC_FIXED_OVERHEAD_MS: 1e4,
|
|
36
|
+
// buffer per sync for local reads + POSTs to the server
|
|
32
37
|
AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
|
|
33
|
-
//
|
|
38
|
+
// floor: minimum timeout for all audit paths
|
|
39
|
+
MAX_AUDIT_MULTI_PATH_TIMEOUT_MS: 18e5,
|
|
40
|
+
// ceiling: 30 minutes, even for many audit paths
|
|
41
|
+
AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS: 1e4,
|
|
42
|
+
// buffer per multi-path audit for local reads + POSTs to the server
|
|
34
43
|
REGISTRY_CONCURRENCY_LIMIT: 5,
|
|
35
44
|
// Limit parallel requests to avoid rate limiting
|
|
36
45
|
HEALTH_CHECK_FETCH_INTERVAL_MS: 6e4,
|
|
@@ -305,6 +314,46 @@ var MonitorClient = class {
|
|
|
305
314
|
await this.flushHealthResults();
|
|
306
315
|
await this.flushSdkErrors();
|
|
307
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Node's fetch() (undici) wraps network-level failures (ECONNREFUSED, DNS lookup
|
|
319
|
+
* failures, TLS errors, ...) in a generic `TypeError: fetch failed`, with the real
|
|
320
|
+
* reason attached via the standard `cause` chain. Without unwrapping it, every
|
|
321
|
+
* network failure across the SDK is reported as the same opaque "fetch failed"
|
|
322
|
+
* message, with no way to diagnose it from the dashboard.
|
|
323
|
+
*/
|
|
324
|
+
describeErrorCause(err, depth = 0) {
|
|
325
|
+
if (depth > 3) return void 0;
|
|
326
|
+
const cause = err.cause;
|
|
327
|
+
if (cause === void 0 || cause === null) return void 0;
|
|
328
|
+
const aggregateErrors = cause.errors;
|
|
329
|
+
if (Array.isArray(aggregateErrors)) {
|
|
330
|
+
const childSummary = aggregateErrors.map((e) => e instanceof Error ? e.message : String(e)).join("; ");
|
|
331
|
+
const summary = cause instanceof Error && cause.message ? `${cause.message} (${childSummary})` : childSummary;
|
|
332
|
+
return this.truncateDescription(summary);
|
|
333
|
+
}
|
|
334
|
+
if (cause instanceof Error) {
|
|
335
|
+
const nested = this.describeErrorCause(cause, depth + 1);
|
|
336
|
+
const description = nested ? `${cause.message} (${nested})` : cause.message;
|
|
337
|
+
return this.truncateDescription(description);
|
|
338
|
+
}
|
|
339
|
+
return this.truncateDescription(String(cause));
|
|
340
|
+
}
|
|
341
|
+
truncateDescription(description) {
|
|
342
|
+
const limit = 500;
|
|
343
|
+
return description.length > limit ? `${description.slice(0, limit)}...` : description;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Run `fn`, swallowing any throw and returning undefined instead. Used to read
|
|
347
|
+
* possibly-hostile Error fields (a throwing message/stack/cause getter) without
|
|
348
|
+
* losing whatever else was already read — each call degrades independently.
|
|
349
|
+
*/
|
|
350
|
+
safe(fn) {
|
|
351
|
+
try {
|
|
352
|
+
return fn();
|
|
353
|
+
} catch {
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
308
357
|
/**
|
|
309
358
|
* Queue an SDK error to be reported to the server's system errors page.
|
|
310
359
|
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
@@ -313,8 +362,16 @@ var MonitorClient = class {
|
|
|
313
362
|
reportError(category, message, err) {
|
|
314
363
|
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
315
364
|
this.sdkErrorsInCurrentWindow++;
|
|
316
|
-
|
|
317
|
-
|
|
365
|
+
let errorMessage = message;
|
|
366
|
+
let stack;
|
|
367
|
+
if (err instanceof Error) {
|
|
368
|
+
errorMessage = this.safe(() => `${message}: ${err.message}`) ?? message;
|
|
369
|
+
const causeDescription = this.safe(() => this.describeErrorCause(err));
|
|
370
|
+
if (causeDescription) {
|
|
371
|
+
errorMessage += ` (cause: ${causeDescription})`;
|
|
372
|
+
}
|
|
373
|
+
stack = this.safe(() => err.stack);
|
|
374
|
+
}
|
|
318
375
|
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
319
376
|
if (this.sdkErrorQueue.length >= 20) {
|
|
320
377
|
this.flushSdkErrors().catch(() => {
|
|
@@ -617,9 +674,11 @@ var MonitorClient = class {
|
|
|
617
674
|
async syncDependencies() {
|
|
618
675
|
console.log("[MonitorClient] Starting technology sync...");
|
|
619
676
|
try {
|
|
677
|
+
const sources = await this.loadDependencySources();
|
|
678
|
+
const timeoutMs = this.calculateSyncTimeoutMs(sources);
|
|
620
679
|
await this.withTimeout(
|
|
621
|
-
(signal) => this.performDependencySync(signal),
|
|
622
|
-
|
|
680
|
+
(signal) => this.performDependencySync(sources, signal),
|
|
681
|
+
timeoutMs,
|
|
623
682
|
"Technology sync timed out"
|
|
624
683
|
);
|
|
625
684
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
@@ -627,25 +686,100 @@ var MonitorClient = class {
|
|
|
627
686
|
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
628
687
|
}
|
|
629
688
|
}
|
|
630
|
-
|
|
689
|
+
/**
|
|
690
|
+
* How many sequential registry batches a version-fetch over this many packages
|
|
691
|
+
* will take, given the shared concurrency limit. Shared by fetchLatestVersions
|
|
692
|
+
* (which does the batching) and calculateSyncTimeoutMs (which budgets for it),
|
|
693
|
+
* so the two can't drift out of sync with each other.
|
|
694
|
+
*/
|
|
695
|
+
registryBatchCount(itemCount) {
|
|
696
|
+
return Math.ceil(itemCount / CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Estimate a timeout budget that scales with how many roughly-equal-cost units of
|
|
700
|
+
* sequential work will run (registry batches, audit paths, ...), clamped to a
|
|
701
|
+
* floor/ceiling. Shared by calculateSyncTimeoutMs and calculateMultiPathAuditTimeoutMs
|
|
702
|
+
* so the two scaling formulas can't drift apart. At count=0 this always collapses to
|
|
703
|
+
* floorMs, since overheadMs alone never exceeds a floor sized for real work.
|
|
704
|
+
*/
|
|
705
|
+
estimateScaledTimeoutMs(count, perItemMs, overheadMs, floorMs, ceilingMs) {
|
|
706
|
+
const estimatedMs = count * perItemMs + overheadMs;
|
|
707
|
+
return Math.min(ceilingMs, Math.max(floorMs, estimatedMs));
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Scale the dependency sync timeout with how many registry lookups AND how many
|
|
711
|
+
* sequential POSTs (one per source, via sendTechnologiesWithEnvironment) it will
|
|
712
|
+
* make, so large dependency lists, many dependencySources, or a large requestTimeoutMs
|
|
713
|
+
* don't spuriously time out against a fixed budget.
|
|
714
|
+
*
|
|
715
|
+
* The POST-phase term (sources.length * requestTimeoutMs) is included even when
|
|
716
|
+
* version checking is off: performDependencySync still does one sequential POST per
|
|
717
|
+
* source regardless, so a flat overhead that ignores source count would under-budget
|
|
718
|
+
* that phase exactly the way a flat registry-batch estimate would under-budget large
|
|
719
|
+
* dependency lists.
|
|
720
|
+
*
|
|
721
|
+
* Registry batches are summed PER SOURCE (not over the combined total) because
|
|
722
|
+
* performDependencySync/fetchLatestVersions batch each source independently and
|
|
723
|
+
* sequentially — ceil(a/n) + ceil(b/n) can be larger than ceil((a+b)/n), so a
|
|
724
|
+
* single batch count over the combined total would under-estimate the real
|
|
725
|
+
* number of sequential registry round-trips when many small sources are configured.
|
|
726
|
+
*/
|
|
727
|
+
calculateSyncTimeoutMs(sources) {
|
|
728
|
+
const postPhaseMs = sources.length * this.requestTimeoutMs;
|
|
729
|
+
const batches = this.versionCheckEnabled ? sources.reduce((sum, source) => sum + this.registryBatchCount(source.technologies.length), 0) : 0;
|
|
730
|
+
return this.estimateScaledTimeoutMs(
|
|
731
|
+
batches,
|
|
732
|
+
this.registryTimeoutMs,
|
|
733
|
+
CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS + postPhaseMs,
|
|
734
|
+
CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
|
|
735
|
+
CONFIG_LIMITS.MAX_SYNC_DEPENDENCIES_TIMEOUT_MS
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Reads happen before syncDependencies' timeout/AbortSignal exists (see comment
|
|
740
|
+
* there), so this phase bounds itself with a wall-clock deadline on a budget that
|
|
741
|
+
* scales with how many sources are configured (mirroring calculateSyncTimeoutMs's
|
|
742
|
+
* scaling principle for the network phase). A plain deadline timestamp — not an
|
|
743
|
+
* AbortController — is enough here: readPackageJsonFromPath does purely synchronous,
|
|
744
|
+
* non-cancelable fs calls, so there is nothing for a signal to actually interrupt;
|
|
745
|
+
* it can only ever be checked between iterations, exactly like a deadline check.
|
|
746
|
+
* Once the budget is spent, stop reading further sources rather than always paying
|
|
747
|
+
* the full read cost for every configured dependencySource regardless of how long
|
|
748
|
+
* it's already taken — and report it via reportError, not just a console.warn, so a
|
|
749
|
+
* truncated sync isn't silently indistinguishable from a fully successful one on
|
|
750
|
+
* the dashboard.
|
|
751
|
+
*/
|
|
752
|
+
async loadDependencySources() {
|
|
631
753
|
if (this.dependencySources && this.dependencySources.length > 0) {
|
|
754
|
+
const sources = [];
|
|
755
|
+
const readBudgetMs = CONFIG_LIMITS.SYNC_FIXED_OVERHEAD_MS * this.dependencySources.length;
|
|
756
|
+
const deadline = Date.now() + readBudgetMs;
|
|
632
757
|
for (const source of this.dependencySources) {
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
758
|
+
if (Date.now() >= deadline) {
|
|
759
|
+
const skippedCount = this.dependencySources.length - sources.length;
|
|
760
|
+
const warning = `Dependency source reads exceeded ${readBudgetMs}ms, skipping remaining ${skippedCount} source(s)`;
|
|
761
|
+
console.warn(`[MonitorClient] ${warning}`);
|
|
762
|
+
this.reportError("DEPENDENCY_SYNC", warning);
|
|
763
|
+
break;
|
|
636
764
|
}
|
|
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}`);
|
|
765
|
+
const technologies2 = await this.readPackageJsonFromPath(source.path);
|
|
766
|
+
sources.push({ environment: source.environment, technologies: technologies2 });
|
|
642
767
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
768
|
+
return sources;
|
|
769
|
+
}
|
|
770
|
+
const technologies = await this.readPackageJson();
|
|
771
|
+
return [{ environment: this.environment, technologies }];
|
|
772
|
+
}
|
|
773
|
+
async performDependencySync(sources, signal) {
|
|
774
|
+
for (const source of sources) {
|
|
775
|
+
if (signal?.aborted) {
|
|
776
|
+
console.log("[MonitorClient] Technology sync cancelled");
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (source.technologies.length === 0) continue;
|
|
780
|
+
const enrichedTechnologies = await this.enrichWithLatestVersions(source.technologies, signal);
|
|
781
|
+
await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
|
|
782
|
+
console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
|
|
649
783
|
}
|
|
650
784
|
}
|
|
651
785
|
/**
|
|
@@ -759,13 +893,22 @@ var MonitorClient = class {
|
|
|
759
893
|
try {
|
|
760
894
|
if (signal?.aborted) return null;
|
|
761
895
|
const encodedName = encodeURIComponent(packageName).replace("%40", "@");
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
896
|
+
const requestController = new AbortController();
|
|
897
|
+
const timeoutId = setTimeout(() => requestController.abort(), this.registryTimeoutMs);
|
|
898
|
+
const onOuterAbort = () => requestController.abort();
|
|
899
|
+
signal?.addEventListener("abort", onOuterAbort);
|
|
900
|
+
try {
|
|
901
|
+
const response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
|
|
902
|
+
headers: { "Accept": "application/json" },
|
|
903
|
+
signal: requestController.signal
|
|
904
|
+
});
|
|
905
|
+
if (!response.ok) return null;
|
|
906
|
+
const data = await response.json();
|
|
907
|
+
return data["dist-tags"]?.latest || null;
|
|
908
|
+
} finally {
|
|
909
|
+
clearTimeout(timeoutId);
|
|
910
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
911
|
+
}
|
|
769
912
|
} catch {
|
|
770
913
|
return null;
|
|
771
914
|
}
|
|
@@ -778,7 +921,7 @@ var MonitorClient = class {
|
|
|
778
921
|
async fetchLatestVersions(packageNames, signal) {
|
|
779
922
|
const results = /* @__PURE__ */ new Map();
|
|
780
923
|
const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
|
|
781
|
-
const totalBatches =
|
|
924
|
+
const totalBatches = this.registryBatchCount(packageNames.length);
|
|
782
925
|
for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
|
|
783
926
|
if (signal?.aborted) {
|
|
784
927
|
console.log("[MonitorClient] Version fetch cancelled");
|
|
@@ -979,7 +1122,7 @@ var MonitorClient = class {
|
|
|
979
1122
|
});
|
|
980
1123
|
proc.on("close", () => {
|
|
981
1124
|
clearTimeout(timeoutId);
|
|
982
|
-
resolve({ stdout, timedOut });
|
|
1125
|
+
resolve({ stdout, stderr, timedOut });
|
|
983
1126
|
});
|
|
984
1127
|
proc.on("error", (err) => {
|
|
985
1128
|
clearTimeout(timeoutId);
|
|
@@ -987,6 +1130,30 @@ var MonitorClient = class {
|
|
|
987
1130
|
});
|
|
988
1131
|
});
|
|
989
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* `npm audit --json` can exit without emitting valid JSON on stdout — e.g. the
|
|
1135
|
+
* registry is unreachable, or a fatal npm error is written to stderr instead.
|
|
1136
|
+
* Treat that as a soft failure (like a timeout) rather than throwing, so it
|
|
1137
|
+
* doesn't surface as a confusing "Unexpected end of JSON input" SDK_ERROR.
|
|
1138
|
+
*/
|
|
1139
|
+
parseNpmAuditJson(auditOutput, stderr) {
|
|
1140
|
+
const stderrSuffix = () => {
|
|
1141
|
+
const trimmedStderr = stderr.trim();
|
|
1142
|
+
return trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : "";
|
|
1143
|
+
};
|
|
1144
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1145
|
+
console.error(`[MonitorClient] npm audit produced no output${stderrSuffix()}`);
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
return JSON.parse(auditOutput);
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
console.error(
|
|
1152
|
+
`[MonitorClient] npm audit produced invalid JSON: ${err instanceof Error ? err.message : String(err)}${stderrSuffix()}`
|
|
1153
|
+
);
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
990
1157
|
/**
|
|
991
1158
|
* Run npm audit and send results to the monitoring server.
|
|
992
1159
|
* This scans the project for known vulnerabilities in dependencies.
|
|
@@ -1059,10 +1226,19 @@ var MonitorClient = class {
|
|
|
1059
1226
|
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1060
1227
|
});
|
|
1061
1228
|
if (result2.timedOut) {
|
|
1062
|
-
|
|
1229
|
+
const message = `yarn audit timed out after ${this.auditTimeoutMs}ms`;
|
|
1230
|
+
console.error(`[MonitorClient] ${message}`);
|
|
1231
|
+
this.reportError("VULNERABILITY_SCAN", message);
|
|
1063
1232
|
return null;
|
|
1064
1233
|
}
|
|
1065
1234
|
auditOutput = result2.stdout;
|
|
1235
|
+
if (!auditOutput || auditOutput.trim().length === 0) {
|
|
1236
|
+
const trimmedStderr = result2.stderr.trim();
|
|
1237
|
+
console.error(
|
|
1238
|
+
`[MonitorClient] yarn audit produced no output${trimmedStderr ? ` (stderr: ${this.truncateDescription(trimmedStderr)})` : ""}`
|
|
1239
|
+
);
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1066
1242
|
vulnerabilities = this.parseYarnAuditOutput(auditOutput);
|
|
1067
1243
|
const lines = auditOutput.trim().split("\n");
|
|
1068
1244
|
for (const line of lines) {
|
|
@@ -1075,33 +1251,24 @@ var MonitorClient = class {
|
|
|
1075
1251
|
} catch {
|
|
1076
1252
|
}
|
|
1077
1253
|
}
|
|
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
1254
|
} else {
|
|
1255
|
+
if (packageManager === "pnpm") {
|
|
1256
|
+
console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
|
|
1257
|
+
}
|
|
1094
1258
|
const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
|
|
1095
1259
|
cwd: projectPath,
|
|
1096
1260
|
timeout: this.auditTimeoutMs,
|
|
1097
1261
|
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
|
|
1098
1262
|
});
|
|
1099
1263
|
if (result2.timedOut) {
|
|
1100
|
-
|
|
1264
|
+
const message = `npm audit timed out after ${this.auditTimeoutMs}ms`;
|
|
1265
|
+
console.error(`[MonitorClient] ${message}`);
|
|
1266
|
+
this.reportError("VULNERABILITY_SCAN", message);
|
|
1101
1267
|
return null;
|
|
1102
1268
|
}
|
|
1103
1269
|
auditOutput = result2.stdout;
|
|
1104
|
-
const auditData =
|
|
1270
|
+
const auditData = this.parseNpmAuditJson(auditOutput, result2.stderr);
|
|
1271
|
+
if (!auditData) return null;
|
|
1105
1272
|
vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
1106
1273
|
totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
1107
1274
|
}
|
|
@@ -1146,9 +1313,10 @@ var MonitorClient = class {
|
|
|
1146
1313
|
}
|
|
1147
1314
|
console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
|
|
1148
1315
|
try {
|
|
1316
|
+
const timeoutMs = this.calculateMultiPathAuditTimeoutMs(this.auditPaths.length);
|
|
1149
1317
|
const result = await this.withTimeout(
|
|
1150
1318
|
() => this.performMultiPathAudit(),
|
|
1151
|
-
|
|
1319
|
+
timeoutMs,
|
|
1152
1320
|
"Multi-path audit timed out"
|
|
1153
1321
|
);
|
|
1154
1322
|
if (result) {
|
|
@@ -1160,6 +1328,21 @@ var MonitorClient = class {
|
|
|
1160
1328
|
return null;
|
|
1161
1329
|
}
|
|
1162
1330
|
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Scale the multi-path audit timeout with how many paths it will scan, so
|
|
1333
|
+
* configuring many auditPaths (or a large per-path auditTimeoutMs) doesn't
|
|
1334
|
+
* spuriously time out the whole batch against a fixed budget — the same
|
|
1335
|
+
* failure mode calculateSyncTimeoutMs fixes for dependency sync.
|
|
1336
|
+
*/
|
|
1337
|
+
calculateMultiPathAuditTimeoutMs(pathCount) {
|
|
1338
|
+
return this.estimateScaledTimeoutMs(
|
|
1339
|
+
pathCount,
|
|
1340
|
+
this.auditTimeoutMs,
|
|
1341
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_FIXED_OVERHEAD_MS,
|
|
1342
|
+
CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
|
|
1343
|
+
CONFIG_LIMITS.MAX_AUDIT_MULTI_PATH_TIMEOUT_MS
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1163
1346
|
async performMultiPathAudit() {
|
|
1164
1347
|
if (!this.auditPaths) return null;
|
|
1165
1348
|
const results = [];
|
package/package.json
CHANGED