@@ -3,6 +3,7 @@ import * as v from 'valibot'
33import { hash } from 'ohash'
44import type { VersionDistributionResponse } from '#shared/types'
55import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
6+ import { encodePackageName } from '#shared/utils/npm'
67import { groupVersionDownloads } from '#server/utils/version-downloads'
78
89/**
@@ -19,7 +20,13 @@ interface NpmVersionDownloadsResponse {
1920 */
2021const QuerySchema = v . object ( {
2122 mode : v . optional ( v . picklist ( [ 'major' , 'minor' ] as const ) , 'major' ) ,
22- filterThreshold : v . optional ( v . pipe ( v . string ( ) , v . transform ( Number ) ) , '1' ) ,
23+ filterThreshold : v . optional (
24+ v . pipe (
25+ v . string ( ) ,
26+ v . toNumber ( ) , // Fails validation on invalid conversion (e.g., "abc") instead of producing NaN
27+ v . minValue ( 0 ) , // Ensure non-negative values
28+ ) ,
29+ ) ,
2330 filterOldVersions : v . optional ( v . picklist ( [ 'true' , 'false' ] as const ) , 'false' ) ,
2431} )
2532
@@ -40,7 +47,8 @@ export default defineCachedEventHandler(
4047 const slugParam = getRouterParam ( event , 'slug' )
4148 const pkgParamSegments = slugParam ?. split ( '/' ) ?? [ ]
4249
43- if ( pkgParamSegments [ pkgParamSegments . length - 1 ] !== 'versions' ) {
50+ const lastSegment = pkgParamSegments . at ( - 1 )
51+ if ( ! lastSegment || lastSegment !== 'versions' ) {
4452 throw createError ( {
4553 statusCode : 404 ,
4654 message : 'Invalid endpoint. Expected /versions' ,
@@ -59,11 +67,15 @@ export default defineCachedEventHandler(
5967 }
6068
6169 const query = getQuery ( event )
62- const { mode, filterThreshold, filterOldVersions } = v . parse ( QuerySchema , query )
63- const filterOldVersionsBool = filterOldVersions === 'true'
70+ const parsed = v . parse ( QuerySchema , query )
71+ const mode = parsed . mode
72+ const filterThreshold = parsed . filterThreshold ?? 1
73+ const filterOldVersionsBool = parsed . filterOldVersions === 'true'
6474
6575 try {
66- const url = `https://api.npmjs.org/versions/${ rawPackageName } /last-week`
76+ // URL-encode package name for scoped packages (e.g., @types/node -> @types%2Fnode)
77+ const encodedPackageName = encodePackageName ( rawPackageName )
78+ const url = `https://api.npmjs.org/versions/${ encodedPackageName } /last-week`
6779 const npmResponse = await fetch ( url )
6880
6981 if ( ! npmResponse . ok ) {
0 commit comments