@@ -22,6 +22,14 @@ const QUERY_SCHEMA = v.object({
2222 label : v . optional ( SafeStringSchema ) ,
2323} )
2424
25+ const EndpointResponseSchema = v . object ( {
26+ schemaVersion : v . literal ( 1 ) ,
27+ label : v . string ( ) ,
28+ message : v . string ( ) ,
29+ color : v . optional ( v . string ( ) ) ,
30+ labelColor : v . optional ( v . string ( ) ) ,
31+ } )
32+
2533const COLORS = {
2634 blue : '#3b82f6' ,
2735 green : '#22c55e' ,
@@ -248,6 +256,18 @@ async function fetchInstallSize(packageName: string, version: string): Promise<n
248256 }
249257}
250258
259+ async function fetchEndpointBadge ( url : string ) {
260+ const response = await fetch ( url , { headers : { Accept : 'application/json' } } )
261+ const data = await response . json ( )
262+ const parsed = v . parse ( EndpointResponseSchema , data )
263+ return {
264+ label : parsed . label ,
265+ value : parsed . message ,
266+ color : parsed . color ,
267+ labelColor : parsed . labelColor ,
268+ }
269+ }
270+
251271const badgeStrategies = {
252272 'version' : async ( pkgData : globalThis . Packument , requestedVersion ?: string ) => {
253273 const version = requestedVersion ?? getLatestVersion ( pkgData ) ?? 'unknown'
@@ -388,65 +408,85 @@ export default defineCachedEventHandler(
388408 async event => {
389409 const query = getQuery ( event )
390410 const typeParam = getRouterParam ( event , 'type' )
391- const pkgParamSegments = getRouterParam ( event , 'pkg' ) ?. split ( '/' ) ?? [ ]
392411
393- if ( pkgParamSegments . length === 0 ) {
394- // TODO: throwing 404 rather than 400 as it's cacheable
395- throw createError ( { statusCode : 404 , message : 'Package name is required.' } )
412+ const queryParams = v . safeParse ( QUERY_SCHEMA , query )
413+ const userColor = queryParams . success ? queryParams . output . color : undefined
414+ const userLabel = queryParams . success ? queryParams . output . label : undefined
415+ const labelColor = queryParams . success ? queryParams . output . labelColor : undefined
416+ const badgeStyleResult = v . safeParse ( BadgeStyleSchema , query . style )
417+ const badgeStyle = badgeStyleResult . success ? badgeStyleResult . output : 'default'
418+
419+ let strategyResult : { label : string ; value : string ; color ?: string ; labelColor ?: string }
420+
421+ if ( typeParam === 'endpoint' ) {
422+ const endpointUrl = typeof query . url === 'string' ? query . url : undefined
423+ if ( ! endpointUrl || ! endpointUrl . startsWith ( 'https://' ) ) {
424+ throw createError ( { statusCode : 400 , message : 'Missing or invalid "url" query parameter.' } )
425+ }
426+
427+ try {
428+ strategyResult = await fetchEndpointBadge ( endpointUrl )
429+ } catch ( error : unknown ) {
430+ handleApiError ( error , { statusCode : 502 , message : 'Failed to fetch endpoint data.' } )
431+ }
432+ } else {
433+ const pkgParamSegments = getRouterParam ( event , 'pkg' ) ?. split ( '/' ) ?? [ ]
434+
435+ if ( pkgParamSegments . length === 0 ) {
436+ // TODO: throwing 404 rather than 400 as it's cacheable
437+ throw createError ( { statusCode : 404 , message : 'Package name is required.' } )
438+ }
439+
440+ const { rawPackageName, rawVersion } = parsePackageParams ( pkgParamSegments )
441+
442+ try {
443+ const { packageName, version : requestedVersion } = v . parse ( PackageRouteParamsSchema , {
444+ packageName : rawPackageName ,
445+ version : rawVersion ,
446+ } )
447+
448+ const showName = queryParams . success && queryParams . output . name === 'true'
449+
450+ const badgeTypeResult = v . safeParse ( BadgeTypeSchema , typeParam )
451+ const strategyKey = badgeTypeResult . success ? badgeTypeResult . output : 'version'
452+ const strategy = badgeStrategies [ strategyKey as keyof typeof badgeStrategies ]
453+
454+ assertValidPackageName ( packageName )
455+
456+ const pkgData = await fetchNpmPackage ( packageName )
457+ const result = await strategy ( pkgData , requestedVersion )
458+ strategyResult = {
459+ label : showName ? packageName : result . label ,
460+ value : result . value ,
461+ color : result . color ,
462+ }
463+ } catch ( error : unknown ) {
464+ handleApiError ( error , {
465+ statusCode : 502 ,
466+ message : ERROR_NPM_FETCH_FAILED ,
467+ } )
468+ }
396469 }
397470
398- const { rawPackageName, rawVersion } = parsePackageParams ( pkgParamSegments )
399-
400- try {
401- const { packageName, version : requestedVersion } = v . parse ( PackageRouteParamsSchema , {
402- packageName : rawPackageName ,
403- version : rawVersion ,
404- } )
405-
406- const queryParams = v . safeParse ( QUERY_SCHEMA , query )
407- const userColor = queryParams . success ? queryParams . output . color : undefined
408- const labelColor = queryParams . success ? queryParams . output . labelColor : undefined
409- const showName = queryParams . success && queryParams . output . name === 'true'
410- const userLabel = queryParams . success ? queryParams . output . label : undefined
411- const badgeStyleResult = v . safeParse ( BadgeStyleSchema , query . style )
412- const badgeStyle = badgeStyleResult . success ? badgeStyleResult . output : 'default'
413-
414- const badgeTypeResult = v . safeParse ( BadgeTypeSchema , typeParam )
415- const strategyKey = badgeTypeResult . success ? badgeTypeResult . output : 'version'
416- const strategy = badgeStrategies [ strategyKey as keyof typeof badgeStrategies ]
417-
418- assertValidPackageName ( packageName )
419-
420- const pkgData = await fetchNpmPackage ( packageName )
421- const strategyResult = await strategy ( pkgData , requestedVersion )
422-
423- const finalLabel = userLabel ? userLabel : showName ? packageName : strategyResult . label
424- const finalValue = strategyResult . value
425-
426- const rawColor = userColor ?? strategyResult . color
427- const finalColor = rawColor ?. startsWith ( '#' ) ? rawColor : `#${ rawColor } `
428-
429- const defaultLabelColor = badgeStyle === 'shieldsio' ? '#555' : '#0a0a0a'
430- const rawLabelColor = labelColor ?? defaultLabelColor
431- const finalLabelColor = rawLabelColor . startsWith ( '#' ) ? rawLabelColor : `#${ rawLabelColor } `
432-
433- const renderFn = badgeStyle === 'shieldsio' ? renderShieldsBadgeSvg : renderDefaultBadgeSvg
434- const svg = renderFn ( { finalColor, finalLabel, finalLabelColor, finalValue } )
435-
436- setHeader ( event , 'Content-Type' , 'image/svg+xml' )
437- setHeader (
438- event ,
439- 'Cache-Control' ,
440- `public, max-age=${ CACHE_MAX_AGE_ONE_HOUR } , s-maxage=${ CACHE_MAX_AGE_ONE_HOUR } ` ,
441- )
442-
443- return svg
444- } catch ( error : unknown ) {
445- handleApiError ( error , {
446- statusCode : 502 ,
447- message : ERROR_NPM_FETCH_FAILED ,
448- } )
449- }
471+ const finalLabel = userLabel ?? strategyResult . label
472+ const finalValue = strategyResult . value
473+ const rawColor = userColor ?? strategyResult . color ?? COLORS . slate
474+ const finalColor = rawColor . startsWith ( '#' ) ? rawColor : `#${ rawColor } `
475+ const defaultLabelColor = badgeStyle === 'shieldsio' ? '#555' : '#0a0a0a'
476+ const rawLabelColor = labelColor ?? strategyResult . labelColor ?? defaultLabelColor
477+ const finalLabelColor = rawLabelColor . startsWith ( '#' ) ? rawLabelColor : `#${ rawLabelColor } `
478+
479+ const renderFn = badgeStyle === 'shieldsio' ? renderShieldsBadgeSvg : renderDefaultBadgeSvg
480+ const svg = renderFn ( { finalColor, finalLabel, finalLabelColor, finalValue } )
481+
482+ setHeader ( event , 'Content-Type' , 'image/svg+xml' )
483+ setHeader (
484+ event ,
485+ 'Cache-Control' ,
486+ `public, max-age=${ CACHE_MAX_AGE_ONE_HOUR } , s-maxage=${ CACHE_MAX_AGE_ONE_HOUR } ` ,
487+ )
488+
489+ return svg
450490 } ,
451491 {
452492 maxAge : CACHE_MAX_AGE_ONE_HOUR ,
0 commit comments