feat: add endpoint badge type for external json data#1796
feat: add endpoint badge type for external json data#1796Moshyfawn wants to merge 5 commits intonpmx-dev:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This PR was sparked by https://bsky.app/profile/davedbase.com/post/3mfz63ldthc2p so both the E2E tests and the docs include the Solid Primitives assets as examples. Let me know if you want npmx specific urls instead we could come up with. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an "Endpoint Badge" feature: documentation and examples; cache logic to return a mock STAGE badge for GitHub raw URLs matching Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
docs/content/2.guide/1.features.mdmodules/runtime/server/cache.tsserver/api/registry/badge/[type]/[...pkg].get.tstest/e2e/badge.spec.ts
| async function fetchEndpointBadge(url: string) { | ||
| const response = await fetch(url, { headers: { Accept: 'application/json' } }) | ||
| if (!response.ok) { | ||
| throw createError({ statusCode: 502, message: `Endpoint returned ${response.status}` }) | ||
| } | ||
| const data = await response.json() | ||
| const parsed = v.parse(EndpointResponseSchema, data) |
There was a problem hiding this comment.
Add timeout and response-size guards for endpoint fetches.
The outbound call is currently unbounded. Slow or very large responses can tie up workers and increase memory pressure.
🧯 Proposed fix
async function fetchEndpointBadge(url: string) {
- const response = await fetch(url, { headers: { Accept: 'application/json' } })
+ const controller = new AbortController()
+ const timeout = setTimeout(() => controller.abort(), 5000)
+ const response = await fetch(url, {
+ headers: { Accept: 'application/json' },
+ signal: controller.signal,
+ })
+ clearTimeout(timeout)
+
if (!response.ok) {
throw createError({ statusCode: 502, message: `Endpoint returned ${response.status}` })
}
+
+ const contentLength = Number(response.headers.get('content-length') ?? 0)
+ if (Number.isFinite(contentLength) && contentLength > 64_000) {
+ throw createError({ statusCode: 502, message: 'Endpoint response is too large.' })
+ }
+
const data = await response.json()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function fetchEndpointBadge(url: string) { | |
| const response = await fetch(url, { headers: { Accept: 'application/json' } }) | |
| if (!response.ok) { | |
| throw createError({ statusCode: 502, message: `Endpoint returned ${response.status}` }) | |
| } | |
| const data = await response.json() | |
| const parsed = v.parse(EndpointResponseSchema, data) | |
| async function fetchEndpointBadge(url: string) { | |
| const controller = new AbortController() | |
| const timeout = setTimeout(() => controller.abort(), 5000) | |
| const response = await fetch(url, { | |
| headers: { Accept: 'application/json' }, | |
| signal: controller.signal, | |
| }) | |
| clearTimeout(timeout) | |
| if (!response.ok) { | |
| throw createError({ statusCode: 502, message: `Endpoint returned ${response.status}` }) | |
| } | |
| const contentLength = Number(response.headers.get('content-length') ?? 0) | |
| if (Number.isFinite(contentLength) && contentLength > 64_000) { | |
| throw createError({ statusCode: 502, message: 'Endpoint response is too large.' }) | |
| } | |
| const data = await response.json() | |
| const parsed = v.parse(EndpointResponseSchema, data) |
| const endpointUrl = typeof query.url === 'string' ? query.url : undefined | ||
| if (!endpointUrl || !endpointUrl.startsWith('https://')) { | ||
| throw createError({ statusCode: 400, message: 'Missing or invalid "url" query parameter.' }) |
There was a problem hiding this comment.
Harden endpoint URL validation to prevent SSRF.
The current https:// prefix check is bypassable for internal targets (e.g. localhost aliases, private hosts, credentialed URLs). Parse and validate the URL structurally before fetch.
🛡️ Proposed fix
+function validateEndpointUrl(input: string): string {
+ let url: URL
+ try {
+ url = new URL(input)
+ } catch {
+ throw createError({ statusCode: 400, message: 'Invalid "url" query parameter.' })
+ }
+
+ if (url.protocol !== 'https:') {
+ throw createError({ statusCode: 400, message: 'Only HTTPS endpoint URLs are allowed.' })
+ }
+
+ if (url.username || url.password) {
+ throw createError({ statusCode: 400, message: 'Credentials are not allowed in endpoint URLs.' })
+ }
+
+ const blockedHosts = new Set(['localhost', '127.0.0.1', '::1'])
+ if (blockedHosts.has(url.hostname)) {
+ throw createError({ statusCode: 400, message: 'Local endpoint URLs are not allowed.' })
+ }
+
+ return url.toString()
+}
+
if (typeParam === 'endpoint') {
const endpointUrl = typeof query.url === 'string' ? query.url : undefined
- if (!endpointUrl || !endpointUrl.startsWith('https://')) {
+ if (!endpointUrl) {
throw createError({ statusCode: 400, message: 'Missing or invalid "url" query parameter.' })
}
+ const validatedEndpointUrl = validateEndpointUrl(endpointUrl)
try {
- strategyResult = await fetchEndpointBadge(endpointUrl)
+ strategyResult = await fetchEndpointBadge(validatedEndpointUrl)
} catch (error: unknown) {
handleApiError(error, { statusCode: 502, message: 'Failed to fetch endpoint data.' })
}There was a problem hiding this comment.
🧹 Nitpick comments (1)
docs/content/2.guide/1.features.md (1)
127-127: Prefer showing a URL-encodedurlparameter in the endpoint badge docs.At Line 158, using an encoded nested URL makes the example safer to copy for endpoints that include their own query string.
Suggested docs tweak
-- **endpoint**: Displays data from an external JSON endpoint via `url` query parameter. :img{src="https://img.shields.io/badge/%2364748b-64748b" class="inline align-middle h-5 w-14"} +- **endpoint**: Displays data from an external JSON endpoint via a URL-encoded `url` query parameter. :img{src="https://img.shields.io/badge/%2364748b-64748b" class="inline align-middle h-5 w-14"} -[](https://github.com/solidjs-community/solid-primitives) +[](https://github.com/solidjs-community/solid-primitives)Also applies to: 156-158
There was a problem hiding this comment.
♻️ Duplicate comments (3)
server/api/registry/badge/[type]/[...pkg].get.ts (3)
271-277:⚠️ Potential issue | 🟠 MajorStill missing timeout and payload-size guards for endpoint fetches.
The outbound call is currently unbounded; slow or very large responses can hold workers and increase memory pressure.
As per coding guidelines: "Use error handling patterns consistently".🧯 Proposed hardening
async function fetchEndpointBadge(url: string) { - const response = await fetch(url, { headers: { Accept: 'application/json' } }) + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 5000) + let response: Response + try { + response = await fetch(url, { + headers: { Accept: 'application/json' }, + signal: controller.signal, + }) + } finally { + clearTimeout(timeout) + } + if (!response.ok) { throw createError({ statusCode: 502, message: `Endpoint returned ${response.status}` }) } + const contentLength = Number(response.headers.get('content-length') ?? 0) + if (Number.isFinite(contentLength) && contentLength > 64_000) { + throw createError({ statusCode: 502, message: 'Endpoint response is too large.' }) + } const data = await response.json() const parsed = v.parse(EndpointResponseSchema, data)
437-444:⚠️ Potential issue | 🔴 CriticalURL validation is still too weak for endpoint mode (SSRF risk).
startsWith('https://')is bypassable for internal or credentialed URLs (https://localhost, private IPs, userinfo). Parse and validate structurally before fetch.🛡️ Proposed validation tightening
+function validateEndpointUrl(input: string): string { + let parsed: URL + try { + parsed = new URL(input) + } catch { + throw createError({ statusCode: 400, message: 'Missing or invalid "url" query parameter.' }) + } + + if (parsed.protocol !== 'https:') { + throw createError({ statusCode: 400, message: 'Only HTTPS endpoint URLs are allowed.' }) + } + if (parsed.username || parsed.password) { + throw createError({ statusCode: 400, message: 'Credentials are not allowed in endpoint URLs.' }) + } + + const host = parsed.hostname.toLowerCase() + if ( + host === 'localhost' + || host.endsWith('.localhost') + || /^127\./.test(host) + || /^10\./.test(host) + || /^192\.168\./.test(host) + || /^172\.(1[6-9]|2\d|3[0-1])\./.test(host) + ) { + throw createError({ statusCode: 400, message: 'Local/private endpoint URLs are not allowed.' }) + } + + return parsed.toString() +} + if (typeParam === 'endpoint') { const endpointUrl = typeof query.url === 'string' ? query.url : undefined - if (!endpointUrl || !endpointUrl.startsWith('https://')) { + if (!endpointUrl) { throw createError({ statusCode: 400, message: 'Missing or invalid "url" query parameter.' }) } + const validatedEndpointUrl = validateEndpointUrl(endpointUrl) try { - strategyResult = await fetchEndpointBadge(endpointUrl) + strategyResult = await fetchEndpointBadge(validatedEndpointUrl) } catch (error: unknown) {
488-492:⚠️ Potential issue | 🟠 MajorNamed endpoint colours are currently broken by
#prefixing.For shields-compatible endpoints, values like
brightgreenare valid; current logic converts them to#brightgreen(invalid), so rendered colours can be wrong.🎨 Proposed colour normalisation fix
+function normaliseBadgeColor(value: string, fallback: string): string { + const trimmed = value.trim() + const hex = trimmed.startsWith('#') ? trimmed.slice(1) : trimmed + + if (/^[0-9a-fA-F]{3,8}$/.test(hex)) { + return `#${hex}` + } + if (/^[a-zA-Z]+$/.test(trimmed)) { + return trimmed + } + return fallback +} + const finalLabel = userLabel ?? strategyResult.label const finalValue = strategyResult.value const rawColor = userColor ?? strategyResult.color ?? COLORS.slate - const finalColor = rawColor.startsWith('#') ? rawColor : `#${rawColor}` + const finalColor = normaliseBadgeColor(rawColor, COLORS.slate) const defaultLabelColor = badgeStyle === 'shieldsio' ? '#555' : '#0a0a0a' const rawLabelColor = labelColor ?? strategyResult.labelColor ?? defaultLabelColor - const finalLabelColor = rawLabelColor.startsWith('#') ? rawLabelColor : `#${rawLabelColor}` + const finalLabelColor = normaliseBadgeColor(rawLabelColor, defaultLabelColor)
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5e16b331-e707-42d8-b717-3e318dd4f084
📒 Files selected for processing (1)
server/api/registry/badge/[type]/[...pkg].get.ts
ghostdevv
left a comment
There was a problem hiding this comment.
What is your usecase @Moshyfawn? The badges we provide are primarily for npmx data as opposed to being a generic badge service, which shields already does really well
🧭 Context
Some projects like Solid Primitives use custom metadata hosted as JSON files on GitHub to display project-specific badges.
📚 Description
Adds an
endpointbadge type that fetches and renders badge data from an external shields.io compatible JSON endpoint.URL format:
/api/registry/badge/endpoint/_?url=https://...