kopia lustrzana https://github.com/badgen/badgen.net
feat: new /memo badge built on Vercel KV (#637)
rodzic
6016e85e70
commit
b22ea33d90
49
api-/memo.ts
49
api-/memo.ts
|
@ -1,49 +0,0 @@
|
||||||
import got from '../libs/got'
|
|
||||||
import { createBadgenHandler, PathArgs } from '../libs/create-badgen-handler'
|
|
||||||
|
|
||||||
const help = `
|
|
||||||
A badge with memories
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Update a badge with a \`PUT\` request:
|
|
||||||
|
|
||||||
curl -X PUT https://badgen.net/memo/a-public-writable-badge/coverage/75%25/orange
|
|
||||||
|
|
||||||
Then you have it:
|
|
||||||
|
|
||||||
https://badgen.net/memo/a-public-writable-badge
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Limits
|
|
||||||
|
|
||||||
Up to 1 write per second per badge.
|
|
||||||
|
|
||||||
## Caveat
|
|
||||||
|
|
||||||
Since everyone can write to any badge, it's recommended to add a uuid suffix to badge name:
|
|
||||||
|
|
||||||
https://badgen.net/memo/my-coverage-badge-df3ff1af-4703-435a-b4ea-20a38e711c7d
|
|
||||||
|
|
||||||
For uuid, you may grab one at https://uuid.now.sh
|
|
||||||
`
|
|
||||||
|
|
||||||
export default createBadgenHandler({
|
|
||||||
title: 'Memo',
|
|
||||||
help,
|
|
||||||
examples: {
|
|
||||||
'/memo/deployed': 'memoized badge for deploy status',
|
|
||||||
},
|
|
||||||
handlers: {
|
|
||||||
'/memo/:name': handler
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handler ({ name }: PathArgs) {
|
|
||||||
const endpoint = `https://badgen-store.amio.workers.dev/${name}`
|
|
||||||
const data = await got(endpoint).json<any>()
|
|
||||||
data.subject = data.subject || data.label
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ import staticBadge from '../pages/api/static'
|
||||||
import github from '../pages/api/github'
|
import github from '../pages/api/github'
|
||||||
import gitlab from '../pages/api/gitlab'
|
import gitlab from '../pages/api/gitlab'
|
||||||
import https from '../pages/api/https'
|
import https from '../pages/api/https'
|
||||||
|
import memo from '../pages/api/memo'
|
||||||
import amo from '../pages/api/amo'
|
import amo from '../pages/api/amo'
|
||||||
import npm from '../pages/api/npm'
|
import npm from '../pages/api/npm'
|
||||||
import pub from '../pages/api/pub'
|
import pub from '../pages/api/pub'
|
||||||
|
@ -29,6 +30,7 @@ export default {
|
||||||
github: github.meta,
|
github: github.meta,
|
||||||
gitlab: gitlab.meta,
|
gitlab: gitlab.meta,
|
||||||
https: https.meta,
|
https: https.meta,
|
||||||
|
memo: memo.meta,
|
||||||
amo: amo.meta,
|
amo: amo.meta,
|
||||||
npm: npm.meta,
|
npm: npm.meta,
|
||||||
pub: pub.meta,
|
pub: pub.meta,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import http from 'http'
|
||||||
import matchRoute from 'my-way'
|
import matchRoute from 'my-way'
|
||||||
|
|
||||||
import { serveBadgeNext } from './serve-badge-next'
|
import { serveBadgeNext } from './serve-badge-next'
|
||||||
import serveDoc from './serve-doc'
|
import serveDoc from './serve-doc-next'
|
||||||
import sentry from './sentry'
|
import sentry from './sentry'
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
@ -11,13 +11,14 @@ import type { BadgenParams } from './types'
|
||||||
import { HTTPError } from 'got'
|
import { HTTPError } from 'got'
|
||||||
|
|
||||||
export type PathArgs = NonNullable<ReturnType<typeof matchRoute>>
|
export type PathArgs = NonNullable<ReturnType<typeof matchRoute>>
|
||||||
export type BadgenResult = Promise<BadgenParams>
|
export type BadgenResponse = BadgenParams | string
|
||||||
|
export type BadgenHandler = (pathArgs: PathArgs, req: NextApiRequest, res: NextApiResponse) => Promise<BadgenResponse>
|
||||||
|
|
||||||
export interface BadgenServeConfig {
|
export interface BadgenServeConfig {
|
||||||
title: string;
|
title: string;
|
||||||
help?: string;
|
help?: string;
|
||||||
examples: { [url: string]: string };
|
examples: { [url: string]: string };
|
||||||
handlers: { [pattern: string]: (pathArgs: PathArgs) => BadgenResult };
|
handlers: { [pattern: string]: BadgenHandler };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
|
export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
|
||||||
|
@ -30,27 +31,32 @@ export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
|
||||||
return res.end()
|
return res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match badge handlers
|
if (matchRoute('/:name', pathname)) {
|
||||||
|
return serveDoc(badgenServerConfig)(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matched badgen handler
|
||||||
let matchedArgs: PathArgs | null = null
|
let matchedArgs: PathArgs | null = null
|
||||||
const matchedScheme = Object.keys(handlers).find(scheme => {
|
const matchedScheme = Object.keys(handlers).find(scheme => {
|
||||||
return matchedArgs = matchRoute(scheme, decodeURI(pathname))
|
return matchedArgs = matchRoute(scheme, decodeURI(pathname))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Invoke badge handler
|
if (matchedArgs === null || matchedScheme === undefined) {
|
||||||
if (matchedArgs !== null && matchedScheme !== undefined) {
|
|
||||||
return await handlers[matchedScheme](matchedArgs).then(params => {
|
|
||||||
return serveBadgeNext(req, res, { params })
|
|
||||||
}).catch(error => {
|
|
||||||
const meta = { matchedArgs, matchedScheme }
|
|
||||||
return onBadgeHandlerError(meta, error, req, res)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchRoute('/:name', pathname)) {
|
|
||||||
return serveDoc(badgenServerConfig)(req, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(404).end()
|
res.status(404).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke matched badgen handler
|
||||||
|
const badgenHandler = handlers[matchedScheme]
|
||||||
|
const badgenResponse = await badgenHandler(matchedArgs, req, res)
|
||||||
|
.catch(error => parseBadgenHandlerError(error, req, res))
|
||||||
|
|
||||||
|
if (typeof badgenResponse === 'string') {
|
||||||
|
res.end(badgenResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serveBadgeNext(req, res, { params: badgenResponse })
|
||||||
}
|
}
|
||||||
|
|
||||||
nextHandler.meta = { title, examples, help, handlers }
|
nextHandler.meta = { title, examples, help, handlers }
|
||||||
|
@ -58,31 +64,31 @@ export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
|
||||||
return nextHandler
|
return nextHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBadgeHandlerError (meta: any, err: Error | HTTPError, req: NextApiRequest, res: NextApiResponse) {
|
function parseBadgenHandlerError (error: Error | HTTPError, req: NextApiRequest, res: NextApiResponse): BadgenResponse {
|
||||||
sentry.captureException(err)
|
sentry.captureException(error)
|
||||||
|
|
||||||
console.error('BADGE_HANDLER_ERROR', err.message, req.url)
|
console.error('BADGE_HANDLER_ERROR', req.url, error.stack || error.message)
|
||||||
|
|
||||||
// Send user friendly response
|
const badgeName = req.url?.split('/')[1]
|
||||||
|
|
||||||
|
// Send user friendly badge response
|
||||||
const errorBadgeParams = {
|
const errorBadgeParams = {
|
||||||
subject: 'error',
|
subject: badgeName || 'error',
|
||||||
status: '500',
|
status: '500',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof HTTPError) {
|
if (error instanceof HTTPError) {
|
||||||
errorBadgeParams.status = err.response.statusCode.toString()
|
errorBadgeParams.status = error.response.statusCode.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof BadgenError) {
|
if (error instanceof BadgenError) {
|
||||||
errorBadgeParams.status = err.status
|
errorBadgeParams.status = error.status
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Error-Message', err.message)
|
res.setHeader('Error-Message', error.message)
|
||||||
return serveBadgeNext(req, res, {
|
|
||||||
code: 200,
|
return errorBadgeParams
|
||||||
params: errorBadgeParams,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBadgeStyle (req: http.IncomingMessage): string | undefined {
|
function getBadgeStyle (req: http.IncomingMessage): string | undefined {
|
||||||
|
|
|
@ -34,8 +34,11 @@ export function serveBadgeNext (req: NextApiRequest, res: NextApiResponse, optio
|
||||||
const badgeSVGString = badgen(badgeParams)
|
const badgeSVGString = badgen(badgeParams)
|
||||||
|
|
||||||
// Minimum s-maxage is set to 300s(5m)
|
// Minimum s-maxage is set to 300s(5m)
|
||||||
|
if (res.getHeader('cache-control') === undefined) {
|
||||||
const cacheMaxAge = cache ? Math.max(parseInt(String(cache)), 300) : sMaxAge
|
const cacheMaxAge = cache ? Math.max(parseInt(String(cache)), 300) : sMaxAge
|
||||||
res.setHeader('Cache-Control', `public, max-age=86400, s-maxage=${cacheMaxAge}, stale-while-revalidate=86400`)
|
res.setHeader('cache-control', `public, max-age=86400, s-maxage=${cacheMaxAge}, stale-while-revalidate=86400`)
|
||||||
|
}
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||||
res.statusCode = code
|
res.statusCode = code
|
||||||
res.send(badgeSVGString)
|
res.send(badgeSVGString)
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
import http from 'http'
|
||||||
|
import matchRoute from 'my-way'
|
||||||
|
import { serveMarked } from 'serve-marked'
|
||||||
|
import serve404 from './serve-404'
|
||||||
|
import { BadgenServeConfig } from './create-badgen-handler-next'
|
||||||
|
|
||||||
|
const { GA_MEASUREMENT_ID = 'G-PD7EFJDYFV' } = process.env
|
||||||
|
|
||||||
|
export default function serveDoc (conf: BadgenServeConfig): http.RequestListener {
|
||||||
|
return (req, res) => {
|
||||||
|
const helpMarkdown = generateHelpMarkdown(conf)
|
||||||
|
|
||||||
|
if (helpMarkdown) {
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400')
|
||||||
|
|
||||||
|
return serveMarked(helpMarkdown, {
|
||||||
|
title: `${conf.title} badge | Badgen`,
|
||||||
|
inlineCSS,
|
||||||
|
beforeHeadEnd: `
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', '${GA_MEASUREMENT_ID}');
|
||||||
|
</script>
|
||||||
|
`,
|
||||||
|
beforeBodyEnd: helpFooter,
|
||||||
|
})(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
serve404(req, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateHelpMarkdown ({ title, help, examples, handlers }: BadgenServeConfig): string {
|
||||||
|
const mainTitle = `# ${title} Badge`
|
||||||
|
|
||||||
|
const customHelp = help || ''
|
||||||
|
|
||||||
|
const exampleTitle = `## Examples`
|
||||||
|
|
||||||
|
const routes = Object.keys(handlers)
|
||||||
|
const categorizedExamples = Object.entries(examples).reduce((accu, [url, desc]) => {
|
||||||
|
const scheme = routes.find(route => matchRoute(route, url))
|
||||||
|
if (scheme) {
|
||||||
|
accu[scheme] ? accu[scheme].push({ url, desc }) : accu[scheme] = [{ url, desc }]
|
||||||
|
}
|
||||||
|
return accu
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const examplesSection = Object.entries(categorizedExamples).reduce((accu, [header, list]) => {
|
||||||
|
const hash = hashify(header)
|
||||||
|
const h4 = `<h4 id="${hash}"><a href="#${hash}"><code>${header.replace(/</g, '<')}</code></a></h4>`
|
||||||
|
const ul = (list as Array<any>).reduce((acc, { url, desc }) => {
|
||||||
|
return `${acc}\n-  [${url}](${url}) <i>${desc}</i>`
|
||||||
|
}, '')
|
||||||
|
return `${accu}\n\n${h4}\n\n${ul}`
|
||||||
|
}, '')
|
||||||
|
|
||||||
|
return [mainTitle, customHelp, exampleTitle, examplesSection].join('\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn `/github/:topic<commits|last-commit>/:owner/:repo/:ref?`
|
||||||
|
// into `github-topic-commits-last-commit-owner-repo-ref`
|
||||||
|
function hashify (str: string) {
|
||||||
|
// return str.replace(/[^\w]/g, '')
|
||||||
|
return str.split(/[^\w]+/).filter(Boolean).join('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
const inlineCSS = `
|
||||||
|
html, body { scroll-behavior: smooth }
|
||||||
|
.markdown-body { max-width: 960px; min-height: calc(100vh - 348px) }
|
||||||
|
.markdown-body h1 { margin-bottom: 42px }
|
||||||
|
li > img { vertical-align: middle; margin: 0.2em 0; font-size: 12px; float: right }
|
||||||
|
li > img + a { font-family: monospace; font-size: 0.9em }
|
||||||
|
li > img + a + i { color: #AAA }
|
||||||
|
h4 a code { color: #333; font-size: 1rem }
|
||||||
|
h4 a:hover { text-decoration: none !important }
|
||||||
|
h4 { padding: 4px 0 }
|
||||||
|
`
|
||||||
|
|
||||||
|
const helpFooter = `
|
||||||
|
<footer>
|
||||||
|
<div class='footer-content'>
|
||||||
|
<div>
|
||||||
|
<h3><img src='/statics/badgen-logo-w.svg' />Badgen Service</h3>
|
||||||
|
<div class='sitemap'>
|
||||||
|
<a href='https://badgen.net'>Classic</a>
|
||||||
|
<em>/</em>
|
||||||
|
<a href='https://flat.badgen.net'>Flat</a>
|
||||||
|
<em>/</em>
|
||||||
|
<a href='/builder'>Builder</a>
|
||||||
|
<em>/</em>
|
||||||
|
<a href='https://github.com/badgen/badgen.net'>GitHub</a>
|
||||||
|
<em>/</em>
|
||||||
|
<a href='https://twitter.com/badgen_net'>Twitter</a>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='bottom'>
|
||||||
|
<div>
|
||||||
|
Built with ♥ by <a href='https://github.com/amio'>Amio</a> and awesome <a href='https://github.com/badgen/badgen.net/graphs/contributors'>contributors</a>. Powered by <a href='https://vercel.com'>Vercel</a>. License under <a href='https://github.com/badgen/badgen.net/blob/master/LICENSE.md'>ISC</a>.
|
||||||
|
</div>
|
||||||
|
<div class='links'>
|
||||||
|
<a href='https://twitter.com/badgen_net'>
|
||||||
|
<img src='https://simpleicons.now.sh/twitter/fff' />
|
||||||
|
</a>
|
||||||
|
<a href='https://github.com/badgen/badgen.net'>
|
||||||
|
<img src='https://simpleicons.now.sh/github/fff' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
margin-top: 5rem;
|
||||||
|
background-color: #222;
|
||||||
|
padding: 2rem 2rem;
|
||||||
|
color: #777;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.footer-content {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
footer h3 {
|
||||||
|
font: 24px/32px Merriweather, serif;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #DDD;
|
||||||
|
}
|
||||||
|
footer h3 img {
|
||||||
|
height: 21px;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-right: 8px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
footer .sitemap {
|
||||||
|
line-height: 26px;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
footer .sitemap a {
|
||||||
|
color: #999;
|
||||||
|
font-family: Merriweather;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
footer a:hover {
|
||||||
|
color: #EEE;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
footer .sitemap em {
|
||||||
|
color: #555;
|
||||||
|
margin: 0 0.6rem;
|
||||||
|
}
|
||||||
|
footer .bottom {
|
||||||
|
margin-top: 2rem;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
padding-top: 2rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 100px;
|
||||||
|
}
|
||||||
|
footer .bottom a {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
footer .links {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
footer .links a {
|
||||||
|
margin-left: 1em;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
footer .links a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
footer .links img {
|
||||||
|
height: 22px;
|
||||||
|
width: 22px
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</footer>
|
||||||
|
`
|
|
@ -24,27 +24,12 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
|
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
const liveBadgeRedirects = badgeList.live.map(badge => {
|
|
||||||
return {
|
|
||||||
source: `/${badge.id}/:path*`,
|
|
||||||
destination: `/api/${badge.id}/:path*`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const staticBadgeRedirects = [{
|
|
||||||
source: `/badge/:path*`,
|
|
||||||
destination: `/api/badge/:path*`,
|
|
||||||
}]
|
|
||||||
|
|
||||||
const badgeRedirects = [
|
|
||||||
{ source: '/badge/:path*', destination: '/api/static' },
|
|
||||||
{ source: '/badge', destination: '/api/static' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const badgeApis = [
|
const badgeApis = [
|
||||||
'/static',
|
'/static',
|
||||||
'/github',
|
'/github',
|
||||||
'/gitlab',
|
'/gitlab',
|
||||||
'/https',
|
'/https',
|
||||||
|
'/memo',
|
||||||
// registry
|
// registry
|
||||||
'/amo',
|
'/amo',
|
||||||
'/npm',
|
'/npm',
|
||||||
|
@ -76,12 +61,14 @@ const nextConfig = {
|
||||||
'/david',
|
'/david',
|
||||||
]
|
]
|
||||||
|
|
||||||
badgeApis.forEach(badge => {
|
let badgeRedirects = [
|
||||||
badgeRedirects.push({ source: `${badge}/:path*`, destination: `/api${badge}` }) // badges
|
{ source: '/badge/:path*', destination: '/api/static' },
|
||||||
badgeRedirects.push({ source: badge, destination: `/api${badge}` }) // doc pages
|
{ source: '/badge', destination: '/api/static' },
|
||||||
})
|
]
|
||||||
|
|
||||||
// const badgeRedirects = liveBadgeRedirects.concat(staticBadgeRedirects)
|
badgeRedirects = badgeRedirects
|
||||||
|
.concat(badgeApis.map(badge => ({ source: `${badge}/:path*`, destination: `/api${badge}` }))) // badges
|
||||||
|
.concat(badgeApis.map(badge => ({ source: badge, destination: `/api${badge}` }))) // doc pages
|
||||||
|
|
||||||
return badgeRedirects
|
return badgeRedirects
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@sentry/nextjs": "^7.37.1",
|
"@sentry/nextjs": "^7.37.1",
|
||||||
"@sentry/tracing": "^7.36.0",
|
"@sentry/tracing": "^7.36.0",
|
||||||
"@vercel/analytics": "^1.0.1",
|
"@vercel/analytics": "^1.0.1",
|
||||||
|
"@vercel/kv": "^0.2.2",
|
||||||
"badgen": "^3.2.2",
|
"badgen": "^3.2.2",
|
||||||
"badgen-icons": "^0.22.0",
|
"badgen-icons": "^0.22.0",
|
||||||
"byte-size": "^8.1.0",
|
"byte-size": "^8.1.0",
|
||||||
|
@ -1373,11 +1374,30 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@upstash/redis": {
|
||||||
|
"version": "1.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.21.0.tgz",
|
||||||
|
"integrity": "sha512-c6M+cl0LOgGK/7Gp6ooMkIZ1IDAJs8zFR+REPkoSkAq38o7CWFX5FYwYEqGZ6wJpUGBuEOr/7hTmippXGgL25A==",
|
||||||
|
"dependencies": {
|
||||||
|
"isomorphic-fetch": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vercel/analytics": {
|
"node_modules/@vercel/analytics": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.1.tgz",
|
||||||
"integrity": "sha512-Ux0c9qUfkcPqng3vrR0GTrlQdqNJ2JREn/2ydrVuKwM3RtMfF2mWX31Ijqo1opSjNAq6rK76PwtANw6kl6TAow=="
|
"integrity": "sha512-Ux0c9qUfkcPqng3vrR0GTrlQdqNJ2JREn/2ydrVuKwM3RtMfF2mWX31Ijqo1opSjNAq6rK76PwtANw6kl6TAow=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@vercel/kv": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-mqnQOB6bkp4h5eObxfLNIlhlVqOGSH8cWOlC5pDVWTjX3zL8dETO1ZBl6M74HBmeBjbD5+J7wDJklRigY6UNKw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@upstash/redis": "1.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.10.0",
|
"version": "8.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||||
|
@ -3804,6 +3824,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/isomorphic-fetch": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"whatwg-fetch": "^3.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -5638,6 +5667,11 @@
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-fetch": {
|
||||||
|
"version": "3.6.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.17.tgz",
|
||||||
|
"integrity": "sha512-c4ghIvG6th0eudYwKZY5keb81wtFz9/WeAHAoy8+r18kcWlitUIrmGFQ2rWEl4UCKUilD3zCLHOIPheHx5ypRQ=="
|
||||||
|
},
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@sentry/nextjs": "^7.37.1",
|
"@sentry/nextjs": "^7.37.1",
|
||||||
"@sentry/tracing": "^7.36.0",
|
"@sentry/tracing": "^7.36.0",
|
||||||
"@vercel/analytics": "^1.0.1",
|
"@vercel/analytics": "^1.0.1",
|
||||||
|
"@vercel/kv": "^0.2.2",
|
||||||
"badgen": "^3.2.2",
|
"badgen": "^3.2.2",
|
||||||
"badgen-icons": "^0.22.0",
|
"badgen-icons": "^0.22.0",
|
||||||
"byte-size": "^8.1.0",
|
"byte-size": "^8.1.0",
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { kv } from '@vercel/kv'
|
||||||
|
import { createBadgenHandler, PathArgs, BadgenResponse } from '../../libs/create-badgen-handler-next'
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
const help = `
|
||||||
|
A badge with memory.
|
||||||
|
|
||||||
|
## Usage (public badge)
|
||||||
|
|
||||||
|
For any <code>/memo/:key</code> badge, like:
|
||||||
|
|
||||||
|
https://badgen.net/memo/my-badge-with-memory
|
||||||
|
|
||||||
|
you may update it with a <code>PUT</code> request:
|
||||||
|
|
||||||
|
curl -X PUT https://badgen.net/memo/my-badge-with-memory/:label/:status/:color
|
||||||
|
|
||||||
|
WARNING: anyone can update this badge, so use it with caution.
|
||||||
|
|
||||||
|
## Usage (protected badge)
|
||||||
|
|
||||||
|
If you want a protected badge (only you can update it), you may add an <code>Authorization: Bearer XXXXXX</code> header while setting it:
|
||||||
|
|
||||||
|
curl -X PUT --header "Authorization: Bearer XXXXXX https://badgen.net/memo/my-badge-with-memory/:label/:status/:color
|
||||||
|
|
||||||
|
Once created, a memo badge created with token can only be updated with the same token, until it's expired.
|
||||||
|
|
||||||
|
## Expiration
|
||||||
|
|
||||||
|
A memo badge will be expired after <b>32 days</b> since it's modified, unless it get updated again within the period.
|
||||||
|
|
||||||
|
- When it's updated, it gets another 32 days lifespan.
|
||||||
|
- When it's expired, it gets cleared like never exists.
|
||||||
|
|
||||||
|
To keep a memo badge, it's recommended to update the badge at least on a monthly basis. Usually this should be done in CI or Cron jobs.
|
||||||
|
`
|
||||||
|
|
||||||
|
export default createBadgenHandler({
|
||||||
|
title: 'Memo',
|
||||||
|
help,
|
||||||
|
examples: {
|
||||||
|
'/memo/deployed': 'memoized badge for deploy status',
|
||||||
|
},
|
||||||
|
handlers: {
|
||||||
|
'/memo/:key': handler,
|
||||||
|
'/memo/:key/:label/:status/:color': putHandler
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const MEMOIZED_TTL_SECONDS = 2764800 // 32 days
|
||||||
|
|
||||||
|
type MemoizedBadgeItem = {
|
||||||
|
token: string;
|
||||||
|
params: {
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handler ({ key }: PathArgs, req: NextApiRequest, res: NextApiResponse): Promise<BadgenResponse> {
|
||||||
|
const storedData = await kv.get<MemoizedBadgeItem>(key)
|
||||||
|
|
||||||
|
if (storedData === null || storedData === undefined) {
|
||||||
|
res.setHeader('cache-control', `s-maxage=1, stale-while-revalidate=1`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: key,
|
||||||
|
status: '404',
|
||||||
|
color: 'grey'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const ttl = await kv.ttl(key)
|
||||||
|
res.setHeader('cache-control', `max-age=${ttl}, s-maxage=300, stale-while-revalidate=86400`)
|
||||||
|
|
||||||
|
const { label, status, color } = storedData.params
|
||||||
|
return { subject: label, status, color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putHandler (args: PathArgs, req: NextApiRequest, res: NextApiResponse): Promise<BadgenResponse> {
|
||||||
|
// Only accept PUT request
|
||||||
|
if (req.method !== 'PUT') {
|
||||||
|
res.setHeader('Allow', 'PUT')
|
||||||
|
res.status(405)
|
||||||
|
return 'Method Not Allowed'
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no token(authorization) is provided,
|
||||||
|
// we will use a default one, which means this badge is public writable.
|
||||||
|
const PUBLIC_TOKEN = 'Bearer PUBLIC_WRITABLE'
|
||||||
|
const token = req.headers['authorization'] || PUBLIC_TOKEN
|
||||||
|
|
||||||
|
const { key, label, status, color } = args
|
||||||
|
|
||||||
|
const newData: MemoizedBadgeItem = { token, params: { label, status, color }}
|
||||||
|
|
||||||
|
const storedData = await kv.get<MemoizedBadgeItem>(key)
|
||||||
|
|
||||||
|
if (storedData === null || storedData.token === token) {
|
||||||
|
// If the key is not found, or found and token is validate, ser/update data and ttl
|
||||||
|
await kv.set(key, newData, { ex: MEMOIZED_TTL_SECONDS })
|
||||||
|
return JSON.stringify(newData.params)
|
||||||
|
} else {
|
||||||
|
// The key is found but token is invalid, refuse to update the data
|
||||||
|
res.status(401)
|
||||||
|
return 'Unauthorized'
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { basename, extname } from 'path'
|
||||||
import { version, versionColor } from '../../libs/utils'
|
import { version, versionColor } from '../../libs/utils'
|
||||||
import { createBadgenHandler } from '../../libs/create-badgen-handler-next'
|
import { createBadgenHandler } from '../../libs/create-badgen-handler-next'
|
||||||
|
|
||||||
import type { PathArgs, BadgenResult } from '../../libs/create-badgen-handler-next'
|
import type { PathArgs, BadgenResponse } from '../../libs/create-badgen-handler-next'
|
||||||
|
|
||||||
const WINGET_GITHUB_REPO = 'microsoft/winget-pkgs'
|
const WINGET_GITHUB_REPO = 'microsoft/winget-pkgs'
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ export default createBadgenHandler({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handler ({ topic, appId }: PathArgs): BadgenResult {
|
async function handler ({ topic, appId }: PathArgs): Promise<BadgenResponse> {
|
||||||
switch (topic) {
|
switch (topic) {
|
||||||
case 'v': {
|
case 'v': {
|
||||||
const versions = await fetchVersions(appId)
|
const versions = await fetchVersions(appId)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import got from '../../libs/got'
|
import got from '../../libs/got'
|
||||||
import { createBadgenHandler } from '../../libs/create-badgen-handler-next'
|
import { createBadgenHandler } from '../../libs/create-badgen-handler-next'
|
||||||
|
|
||||||
import type { PathArgs, BadgenResult } from '../../libs/create-badgen-handler-next'
|
import type { PathArgs, BadgenResponse } from '../../libs/create-badgen-handler-next'
|
||||||
|
|
||||||
export default createBadgenHandler({
|
export default createBadgenHandler({
|
||||||
title: 'XO',
|
title: 'XO',
|
||||||
|
@ -24,7 +24,7 @@ const getIndent = space => {
|
||||||
return `${space} spaces`
|
return `${space} spaces`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handler ({ topic, scope, name }: PathArgs): BadgenResult {
|
async function handler ({ topic, scope, name }: PathArgs): Promise<BadgenResponse> {
|
||||||
const pkg = scope ? `${scope}/${name}` : name
|
const pkg = scope ? `${scope}/${name}` : name
|
||||||
const endpoint = `https://cdn.jsdelivr.net/npm/${pkg}/package.json`
|
const endpoint = `https://cdn.jsdelivr.net/npm/${pkg}/package.json`
|
||||||
const data = await got(endpoint).json<any>()
|
const data = await got(endpoint).json<any>()
|
||||||
|
|
|
@ -108,10 +108,6 @@
|
||||||
"source": "/melpa/:match*",
|
"source": "/melpa/:match*",
|
||||||
"destination": "https://v2022.badgen.net/melpa/:match*"
|
"destination": "https://v2022.badgen.net/melpa/:match*"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"source": "/memo/:match*",
|
|
||||||
"destination": "https://v2022.badgen.net/memo/:match*"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"source": "/nuget/:match*",
|
"source": "/nuget/:match*",
|
||||||
"destination": "https://v2022.badgen.net/nuget/:match*"
|
"destination": "https://v2022.badgen.net/nuget/:match*"
|
||||||
|
|
Ładowanie…
Reference in New Issue