[READ-ONLY] a fast, modern browser for the npm registry
at main 123 lines 4.1 kB view raw
1import type { PackageVersionInfo, PublishTrustLevel } from '#shared/types' 2import { compare, major } from 'semver' 3 4export interface PublishSecurityDowngrade { 5 downgradedVersion: string 6 downgradedPublishedAt?: string 7 downgradedTrustLevel: PublishTrustLevel 8 /** Recommended trusted version within the same major, if one exists */ 9 trustedVersion?: string 10 trustedPublishedAt?: string 11 trustedTrustLevel: PublishTrustLevel 12} 13 14type VersionWithIndex = PackageVersionInfo & { 15 index: number 16 timestamp: number 17 trustRank: number 18 resolvedTrustLevel: PublishTrustLevel 19} 20 21const TRUST_RANK: Record<PublishTrustLevel, number> = { 22 none: 0, 23 provenance: 1, 24 trustedPublisher: 2, 25} 26 27function resolveTrustLevel(version: PackageVersionInfo): PublishTrustLevel { 28 if (version.trustLevel) return version.trustLevel 29 // Fallback for legacy data: hasProvenance only indicates non-'none' trust, 30 // so map it to provenance (the lower rank) to avoid over-ranking 31 return version.hasProvenance ? 'provenance' : 'none' 32} 33 34function toTimestamp(time?: string): number { 35 if (!time) return Number.NaN 36 return Date.parse(time) 37} 38 39function sortByRecency(a: VersionWithIndex, b: VersionWithIndex): number { 40 const aValid = !Number.isNaN(a.timestamp) 41 const bValid = !Number.isNaN(b.timestamp) 42 43 if (!aValid && !bValid) { 44 // Fall back to semver comparison if no valid timestamps 45 const semverOrder = compare(b.version, a.version) 46 if (semverOrder !== 0) return semverOrder 47 48 // If semver is also equal, maintain original order 49 return a.index - b.index 50 } 51 52 if (aValid !== bValid) { 53 return aValid ? -1 : 1 54 } 55 56 return b.timestamp - a.timestamp 57} 58 59/** 60 * Detects a security downgrade for a specific viewed version. 61 * A version is considered downgraded when it has no provenance and 62 * there exists an older trusted release. 63 */ 64export function detectPublishSecurityDowngradeForVersion( 65 versions: PackageVersionInfo[], 66 viewedVersion: string, 67): PublishSecurityDowngrade | null { 68 if (versions.length < 2 || !viewedVersion) return null 69 70 const sorted = versions 71 .map((version, index) => { 72 const resolvedTrustLevel = resolveTrustLevel(version) 73 return { 74 ...version, 75 index, 76 timestamp: toTimestamp(version.time), 77 trustRank: TRUST_RANK[resolvedTrustLevel], 78 resolvedTrustLevel, 79 } 80 }) 81 .sort(sortByRecency) 82 83 const currentIndex = sorted.findIndex(version => version.version === viewedVersion) 84 if (currentIndex === -1) return null 85 86 const current = sorted[currentIndex] 87 if (!current) return null 88 89 const currentMajor = major(current.version) 90 91 // Find the strongest older version across all majors (for detection) 92 // and the strongest within the same major (for recommendation) 93 let strongestOlderAny: VersionWithIndex | null = null 94 let strongestOlderSameMajor: VersionWithIndex | null = null 95 for (const version of sorted.slice(currentIndex + 1)) { 96 // Skip deprecated versions — recommending a deprecated version is misleading 97 if (version.deprecated) continue 98 if (!strongestOlderAny || version.trustRank > strongestOlderAny.trustRank) { 99 strongestOlderAny = version 100 } 101 if (major(version.version) === currentMajor) { 102 if (!strongestOlderSameMajor || version.trustRank > strongestOlderSameMajor.trustRank) { 103 strongestOlderSameMajor = version 104 } 105 } 106 } 107 108 // Use same-major for recommendation if available, otherwise any-major for detection only 109 const strongestOlder = strongestOlderSameMajor ?? strongestOlderAny 110 if (!strongestOlder || strongestOlder.trustRank <= current.trustRank) return null 111 112 // Only recommend a specific version if it's in the same major 113 const recommendation = strongestOlderSameMajor 114 115 return { 116 downgradedVersion: current.version, 117 downgradedPublishedAt: current.time, 118 downgradedTrustLevel: current.resolvedTrustLevel, 119 trustedVersion: recommendation?.version, 120 trustedPublishedAt: recommendation?.time, 121 trustedTrustLevel: strongestOlder.resolvedTrustLevel, 122 } 123}