[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 292 lines 9.6 kB view raw
1import { describe, expect, it } from 'vitest' 2import type { Packument, PackageVersionInfo } from '#shared/types' 3import { transformPackument } from '~/composables/npm/usePackage' 4import { detectPublishSecurityDowngradeForVersion } from '~/utils/publish-security' 5 6function createVersion(version: string, hasAttestations = false): Packument['versions'][string] { 7 return { 8 _id: `foo@${version}`, 9 _npmVersion: '10.0.0', 10 name: 'foo', 11 version, 12 dist: { 13 shasum: version, 14 tarball: `https://registry.npmjs.org/foo/-/foo-${version}.tgz`, 15 signatures: [], 16 ...(hasAttestations 17 ? { 18 attestations: { 19 url: `https://example.test/${version}`, 20 provenance: { predicateType: 'https://slsa.dev/provenance/v1' }, 21 }, 22 } 23 : {}), 24 }, 25 } 26} 27 28function createTrustedPublisherVersion(version: string) { 29 return { 30 ...createVersion(version, false), 31 _npmUser: { 32 name: 'github-actions', 33 email: 'noreply@github.com', 34 trustedPublisher: { 35 id: 'github', 36 }, 37 }, 38 } 39} 40 41function createTrustedPublisherWithAttestationsVersion(version: string) { 42 return { 43 ...createVersion(version, true), 44 _npmUser: { 45 name: 'github-actions', 46 email: 'noreply@github.com', 47 trustedPublisher: { 48 id: 'github', 49 }, 50 }, 51 } 52} 53 54function createPackument( 55 versions: Packument['versions'], 56 time: Packument['time'], 57 latest: string, 58): Packument { 59 return { 60 '_id': 'foo', 61 '_rev': '1', 62 'name': 'foo', 63 'dist-tags': { latest }, 64 time, 65 versions, 66 } 67} 68 69function toVersionInfos(packument: ReturnType<typeof transformPackument>): PackageVersionInfo[] { 70 return ( 71 packument.securityVersions ?? 72 Object.entries(packument.versions).map(([version, metadata]) => ({ 73 version, 74 time: packument.time[version], 75 hasProvenance: !!metadata.hasProvenance, 76 trustLevel: metadata.trustLevel, 77 deprecated: metadata.deprecated, 78 })) 79 ) 80} 81 82describe('transformPackument', () => { 83 it('includes requested old version and preserves provenance on it', () => { 84 const packument = createPackument( 85 { 86 '1.0.0': createVersion('1.0.0', true), 87 '1.0.1': createVersion('1.0.1'), 88 '1.0.2': createVersion('1.0.2'), 89 '1.0.3': createVersion('1.0.3'), 90 '1.0.4': createVersion('1.0.4'), 91 '1.0.5': createVersion('1.0.5'), 92 '1.0.6': createVersion('1.0.6'), 93 '1.0.7': createVersion('1.0.7'), 94 }, 95 { 96 'created': '2026-01-01T00:00:00.000Z', 97 'modified': '2026-01-08T00:00:00.000Z', 98 '1.0.0': '2026-01-01T00:00:00.000Z', 99 '1.0.1': '2026-01-02T00:00:00.000Z', 100 '1.0.2': '2026-01-03T00:00:00.000Z', 101 '1.0.3': '2026-01-04T00:00:00.000Z', 102 '1.0.4': '2026-01-05T00:00:00.000Z', 103 '1.0.5': '2026-01-06T00:00:00.000Z', 104 '1.0.6': '2026-01-07T00:00:00.000Z', 105 '1.0.7': '2026-01-08T00:00:00.000Z', 106 }, 107 '1.0.7', 108 ) 109 110 const transformed = transformPackument(packument, '1.0.0') 111 112 expect(transformed.versions['1.0.0']?.hasProvenance).toBe(true) 113 expect(transformed.versions['1.0.1']).toBeUndefined() 114 expect(transformed.versions['1.0.2']).toBeUndefined() 115 expect(transformed.securityVersions).toHaveLength(8) 116 }) 117 118 it('omits securityVersions when all versions have the same trust level', () => { 119 const packument = createPackument( 120 { 121 '1.0.0': createVersion('1.0.0'), 122 '1.0.1': createVersion('1.0.1'), 123 '1.0.2': createVersion('1.0.2'), 124 }, 125 { 126 'created': '2026-01-01T00:00:00.000Z', 127 'modified': '2026-01-03T00:00:00.000Z', 128 '1.0.0': '2026-01-01T00:00:00.000Z', 129 '1.0.1': '2026-01-02T00:00:00.000Z', 130 '1.0.2': '2026-01-03T00:00:00.000Z', 131 }, 132 '1.0.2', 133 ) 134 135 const transformed = transformPackument(packument, '1.0.2') 136 137 // All versions have trustLevel 'none', so no mixed trust — omit the array 138 expect(transformed.securityVersions).toBeUndefined() 139 }) 140 141 it('includes securityVersions when package has mixed trust levels', () => { 142 const packument = createPackument( 143 { 144 '1.0.0': createVersion('1.0.0', true), 145 '1.0.1': createVersion('1.0.1'), 146 }, 147 { 148 'created': '2026-01-01T00:00:00.000Z', 149 'modified': '2026-01-02T00:00:00.000Z', 150 '1.0.0': '2026-01-01T00:00:00.000Z', 151 '1.0.1': '2026-01-02T00:00:00.000Z', 152 }, 153 '1.0.1', 154 ) 155 156 const transformed = transformPackument(packument, '1.0.1') 157 158 expect(transformed.securityVersions).toHaveLength(2) 159 }) 160 161 it('works with downgrade detection for viewed version', () => { 162 const packument = createPackument( 163 { 164 '1.0.0': createVersion('1.0.0', true), 165 '1.0.1': createVersion('1.0.1'), 166 '1.0.2': createVersion('1.0.2', true), 167 }, 168 { 169 'created': '2026-01-01T00:00:00.000Z', 170 'modified': '2026-01-03T00:00:00.000Z', 171 '1.0.0': '2026-01-01T00:00:00.000Z', 172 '1.0.1': '2026-01-02T00:00:00.000Z', 173 '1.0.2': '2026-01-03T00:00:00.000Z', 174 }, 175 '1.0.2', 176 ) 177 178 const transformed = transformPackument(packument, '1.0.1') 179 const infos = toVersionInfos(transformed) 180 181 expect(detectPublishSecurityDowngradeForVersion(infos, '1.0.2')).toBeNull() 182 expect(detectPublishSecurityDowngradeForVersion(infos, '1.0.1')).toEqual({ 183 downgradedVersion: '1.0.1', 184 downgradedPublishedAt: '2026-01-02T00:00:00.000Z', 185 downgradedTrustLevel: 'none', 186 trustedVersion: '1.0.0', 187 trustedPublishedAt: '2026-01-01T00:00:00.000Z', 188 trustedTrustLevel: 'provenance', 189 }) 190 }) 191 192 it('treats trustedPublisher as trust evidence for downgrade checks', () => { 193 const packument = createPackument( 194 { 195 '1.0.0': createTrustedPublisherVersion('1.0.0'), 196 '1.0.1': createVersion('1.0.1'), 197 '1.0.2': createVersion('1.0.2'), 198 }, 199 { 200 'created': '2026-01-01T00:00:00.000Z', 201 'modified': '2026-01-03T00:00:00.000Z', 202 '1.0.0': '2026-01-01T00:00:00.000Z', 203 '1.0.1': '2026-01-02T00:00:00.000Z', 204 '1.0.2': '2026-01-03T00:00:00.000Z', 205 }, 206 '1.0.2', 207 ) 208 209 const transformed = transformPackument(packument, '1.0.1') 210 const infos = toVersionInfos(transformed) 211 212 expect(infos.find(v => v.version === '1.0.0')?.hasProvenance).toBe(true) 213 expect(detectPublishSecurityDowngradeForVersion(infos, '1.0.1')?.trustedVersion).toBe('1.0.0') 214 }) 215 216 it('prefers trustedPublisher trust level when both trustedPublisher and attestations exist', () => { 217 const packument = createPackument( 218 { 219 '1.0.0': createTrustedPublisherWithAttestationsVersion('1.0.0'), 220 '1.0.1': createTrustedPublisherVersion('1.0.1'), 221 }, 222 { 223 'created': '2026-01-01T00:00:00.000Z', 224 'modified': '2026-01-02T00:00:00.000Z', 225 '1.0.0': '2026-01-01T00:00:00.000Z', 226 '1.0.1': '2026-01-02T00:00:00.000Z', 227 }, 228 '1.0.1', 229 ) 230 231 const transformed = transformPackument(packument, '1.0.1') 232 233 expect(transformed.versions['1.0.0']?.trustLevel).toBe('trustedPublisher') 234 }) 235 236 // https://github.com/npmx-dev/npmx.dev/issues/1292 237 it('does not flag false downgrade when trusted publisher version also has attestations', () => { 238 // Trusted publishing automatically generates provenance attestations, 239 // so a version with both should be classified as trustedPublisher, not provenance. 240 const packument = createPackument( 241 { 242 '7.0.0': createTrustedPublisherWithAttestationsVersion('7.0.0'), 243 '7.0.1': createTrustedPublisherWithAttestationsVersion('7.0.1'), 244 }, 245 { 246 'created': '2026-01-01T00:00:00.000Z', 247 'modified': '2026-01-02T00:00:00.000Z', 248 '7.0.0': '2026-01-01T00:00:00.000Z', 249 '7.0.1': '2026-01-02T00:00:00.000Z', 250 }, 251 '7.0.1', 252 ) 253 254 const transformed = transformPackument(packument, '7.0.1') 255 const infos = toVersionInfos(transformed) 256 257 // Both versions should be trustedPublisher — no downgrade 258 expect(infos.find(v => v.version === '7.0.0')?.trustLevel).toBe('trustedPublisher') 259 expect(infos.find(v => v.version === '7.0.1')?.trustLevel).toBe('trustedPublisher') 260 expect(detectPublishSecurityDowngradeForVersion(infos, '7.0.1')).toBeNull() 261 }) 262 263 it('flags non-direct downgrade chain until trust is restored', () => { 264 const packument = createPackument( 265 { 266 '2.1.0': createVersion('2.1.0', true), 267 '2.1.1': createVersion('2.1.1'), 268 '2.2.0': createVersion('2.2.0'), 269 '2.3.0': createVersion('2.3.0'), 270 '2.4.0': createVersion('2.4.0', true), 271 }, 272 { 273 'created': '2026-01-01T00:00:00.000Z', 274 'modified': '2026-01-05T00:00:00.000Z', 275 '2.1.0': '2026-01-01T00:00:00.000Z', 276 '2.1.1': '2026-01-02T00:00:00.000Z', 277 '2.2.0': '2026-01-03T00:00:00.000Z', 278 '2.3.0': '2026-01-04T00:00:00.000Z', 279 '2.4.0': '2026-01-05T00:00:00.000Z', 280 }, 281 '2.4.0', 282 ) 283 284 const transformed = transformPackument(packument, '2.3.0') 285 const infos = toVersionInfos(transformed) 286 287 expect(detectPublishSecurityDowngradeForVersion(infos, '2.1.1')?.trustedVersion).toBe('2.1.0') 288 expect(detectPublishSecurityDowngradeForVersion(infos, '2.2.0')?.trustedVersion).toBe('2.1.0') 289 expect(detectPublishSecurityDowngradeForVersion(infos, '2.3.0')?.trustedVersion).toBe('2.1.0') 290 expect(detectPublishSecurityDowngradeForVersion(infos, '2.4.0')).toBeNull() 291 }) 292})