@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 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
- // 60 seconds max for all dependency syncs
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
- // 3 minutes max for all audit paths
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
- const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
353
- const stack = err instanceof Error ? err.stack : void 0;
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
- CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
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
- async performDependencySync(signal) {
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 (signal?.aborted) {
670
- console.log("[MonitorClient] Technology sync cancelled");
671
- return;
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 technologies = await this.readPackageJsonFromPath(source.path);
674
- if (technologies.length === 0) continue;
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
- } else {
680
- const technologies = await this.readPackageJson();
681
- if (technologies.length === 0) return;
682
- const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
683
- await this.sendTechnologies(enrichedTechnologies);
684
- console.log(`[MonitorClient] Technology sync completed for ${this.environment}`);
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 response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
799
- headers: { "Accept": "application/json" },
800
- signal: signal ?? AbortSignal.timeout(this.registryTimeoutMs)
801
- });
802
- if (!response.ok) return null;
803
- const data = await response.json();
804
- return data["dist-tags"]?.latest || null;
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 = Math.ceil(packageNames.length / concurrencyLimit);
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 = JSON.parse(auditOutput);
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
- CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
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
- // 60 seconds max for all dependency syncs
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
- // 3 minutes max for all audit paths
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
- const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
317
- const stack = err instanceof Error ? err.stack : void 0;
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
- CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
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
- async performDependencySync(signal) {
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 (signal?.aborted) {
634
- console.log("[MonitorClient] Technology sync cancelled");
635
- return;
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 technologies = await this.readPackageJsonFromPath(source.path);
638
- if (technologies.length === 0) continue;
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
- } else {
644
- const technologies = await this.readPackageJson();
645
- if (technologies.length === 0) return;
646
- const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
647
- await this.sendTechnologies(enrichedTechnologies);
648
- console.log(`[MonitorClient] Technology sync completed for ${this.environment}`);
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 response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
763
- headers: { "Accept": "application/json" },
764
- signal: signal ?? AbortSignal.timeout(this.registryTimeoutMs)
765
- });
766
- if (!response.ok) return null;
767
- const data = await response.json();
768
- return data["dist-tags"]?.latest || null;
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 = Math.ceil(packageNames.length / concurrencyLimit);
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 = JSON.parse(auditOutput);
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
- CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceon-oy/monitor-sdk",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Client SDK for Ceon Monitor - Error tracking, health monitoring, security events, and vulnerability scanning",
5
5
  "author": "Ceon",
6
6
  "license": "MIT",