npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gitlab-badge-proxy

v0.1.4

Published

Server-side proxy that turns private GitLab pipeline/coverage status into public SVG badges, without ever exposing the GitLab instance's URL to clients.

Readme

gitlab-badge-proxy

A tiny Hono app that turns a private GitLab project's pipeline/coverage status into public SVG badges, without ever exposing the GitLab instance's hostname or an access token to the browser. Everything that touches the real GitLab URL happens server-side, inside the deployed function; the client only ever sees your own domain.

Why

GitLab's own badge images (/<namespace>/<project>/badges/<branch>/pipeline.svg) require a logged-in session to view on a private project — there is no supported token query parameter for them (that was disabled upstream for security reasons). This proxy instead calls the GitLab REST API (which does accept a PRIVATE-TOKEN header) to read the latest pipeline status/coverage, then re-renders that as a shields.io static badge, fetched and streamed server-side. The GitLab URL and token never appear in any client-visible response, header, or redirect.

Routes

  • GET /pipeline.svg — latest pipeline status on the configured ref (passing / failing / running / unknown).
  • GET /coverage.svg — coverage percentage of the latest pipeline on that ref, color-graded. Add ?job=<job-name> to scope it to one job's coverage instead of the pipeline's aggregate, same as GitLab's own native badge query param.
  • GET /release.svg — latest release's tag (version badge, blue for stable / orange for 0.x or pre-release-like tags), same coloring convention as shields.io's version badges.
  • GET /tag.svg — most recently updated tag, same version-badge coloring as release.svg.
  • GET /stars.svg, GET /forks.svg, GET /contributors.svg — repository counts (abbreviated for large numbers, e.g. 12.3k).
  • GET /issues.svg?state=opened|closed|all&labels=<comma,separated> — issue count for the given state (default all); green when the count is zero, yellow otherwise.
  • GET /merge-requests.svg?state=opened|closed|merged|locked|all&labels=<comma,separated> — merge request count for the given state (default opened); shows "more than 10k" past GitLab's own count-header limit.
  • GET /last-commit.svg?ref=<ref>&path=<file> — how long ago the most recent matching commit landed (ref defaults to the configured GITLAB_REF).
  • GET /license.svg — the project's detected license name, or "not specified".
  • GET /languages.svg — the top language by percentage of code. GET /languages-count.svg — how many distinct languages GitLab detected.
  • GET /labels.svg?name=<label-name> — renders that label's own color from GitLab.
  • GET /go-mod.svg?filename=go.mod&ref=<ref> — the Go version declared in go.mod (or another path given via filename), as a version badge.
  • GET /health — liveness check; does not read GitLab configuration.

On any failure (GitLab unreachable, bad token, pipeline has no coverage) the routes degrade to a grey "unknown" badge instead of erroring, so a broken/rotated token shows up as a stale badge, not a broken image or a leaked error message.

Configuration

All configuration is env-only — nothing GitLab-specific is hardcoded in the source, so this package stays safe to publish and reuse. Copy .env.example and fill in your own values (as Vercel project env vars in production, never committed):

| Variable | Required | Description | |---|---|---| | GITLAB_URL | yes | Base URL of your GitLab instance. | | GITLAB_TOKEN | yes | Token with the read_api scope on the target project only. | | GITLAB_PROJECT | yes | Project path (group/name) or numeric ID. | | GITLAB_REF | no | Branch/ref to report on. Defaults to master. | | CACHE_MAX_AGE_SECONDS | no | Edge cache lifetime for successful responses. Defaults to 60. | | GITLAB_PROJECTS | no | Comma-separated whitelist for the GitLab-shaped routes below. Empty by default. |

GitLab-compatible badge URLs

GitLab's own badges live at <gitlab-url>/<namespace>/<project>/badges/<ref>/pipeline.svg (and coverage.svg), plus <gitlab-url>/<namespace>/<project>/-/badges/release.svg for the latest release (no ref — releases aren't tied to a branch). This proxy serves the same path shapes, so existing badge links keep working when you swap the GitLab host for this proxy's — e.g. https://badges.yourdomain.tld/ardenslab/pm2-dash/badges/main/coverage.svg or https://badges.yourdomain.tld/ardenslab/pm2-dash/-/badges/release.svg.

For security, only project paths listed in GITLAB_PROJECTS are served this way; any other path (or one that doesn't match /badges/<ref>/pipeline.svg, /badges/<ref>/coverage.svg, or /-/badges/release.svg) returns a plain 404, the same as GitLab's own behavior for an unknown project — the token is never used to probe or confirm the existence of a project you haven't explicitly whitelisted. The ref segment is free-form (any branch of a whitelisted project), unlike the fixed GITLAB_REF used by the flat /pipeline.svg / /coverage.svg routes above. The coverage route also accepts ?job=<job-name>, same as the flat route.

Project-scoped badges beyond pipeline/coverage/release

The rest of the Routes above — stars, forks, contributors, issues, merge requests, last commit, license, tag, languages, language count, labels, go.mod version — aren't part of GitLab's own badge system, so there's no GitLab-native URL shape to mirror for them. This proxy has its own: GET /<group/project>/badges/<name>.svg, e.g. https://badges.yourdomain.tld/ardenslab/pm2-dash/badges/stars.svg or .../badges/issues.svg?state=opened&labels=bug. <name> is one of stars, forks, contributors, issues, merge-requests, last-commit, license, tag, languages, languages-count, labels, go-mod — same query params as the matching flat route (labels.svg still needs ?name=). Gated by the same GITLAB_PROJECTS whitelist as the GitLab-shaped routes above; an unlisted project 404s the same way.

Badge preview page

GET /debug/<group/project>?ref=<branch> renders an HTML page with every badge this proxy can produce for that project: its own GitLab-shaped routes, the fixed-default-project flat routes, and the shields.io badges that work through the gitlab_url emulation below — all as live <img> tags, so you can eyeball everything at once instead of constructing and testing each URL by hand. GET /debug alone redirects to the configured default project. Each embedded badge still goes through its own route's whitelist check when the browser loads it, so an unlisted project just shows 404s in place, same as using that badge URL directly.

GitLab API emulation (for shields.io and similar)

Some badge generators — notably shields.io's generic gitlab badges (/gitlab/v/release/<project>, /gitlab/v/tag/<project>, etc.) — don't fetch a pre-rendered badge; instead they take a gitlab_url query param and build their own GitLab REST API calls against it (<gitlab_url>/api/v4/projects/<project>/releases, etc.). Pointing gitlab_url at your real GitLab instance would require that instance to be publicly reachable. Point it at this proxy instead, and it answers those specific calls on the real GitLab's behalf:

https://img.shields.io/gitlab/v/release/ardenslab%2Fpm2-dash?gitlab_url=https%3A%2F%2Fbadges.yourdomain.tld
https://img.shields.io/gitlab/v/tag/ardenslab%2Fpm2-dash?gitlab_url=https%3A%2F%2Fbadges.yourdomain.tld

(Leave off the trailing slash on gitlab_url — with one, shields.io's own path gets appended with a doubled /; the proxy redirects to collapse it, but it's one fewer hop to just omit it.)

Only a narrow, read-only slice of the real API is implemented — releases, repository/tags, languages, issues_statistics, and pipelines/pipelines/:id/pipelines/:id/jobs — covering the version/tag/pipeline/coverage/issue-count badge shapes. Same whitelist as the GitLab-shaped badge routes above (GITLAB_PROJECTS): an unlisted project, or any subpath not on that list (file contents, full issue/MR bodies, commit messages, contributor emails — anything with actual content rather than metadata), 404s instead of being proxied. Every response body also has the real GITLAB_URL stripped out first, since GitLab embeds it in web_url/_links on nearly every resource.

Installing as a library

npm install gitlab-badge-proxy

The package exports a ready-made Hono app plus the pieces used to build it:

import { app } from "gitlab-badge-proxy";
// app.request(...) / app.fetch(...) — mount it directly, or compose it into a bigger Hono app.
import { createApp, loadConfig, type Config } from "gitlab-badge-proxy";

// Supply config explicitly instead of reading env vars:
const config: Config = {
  gitlabUrl: "https://gitlab.example.net",
  gitlabToken: process.env.MY_TOKEN!,
  project: "group/project",
  ref: "main",
  cacheMaxAgeSeconds: 60,
};
const app = createApp(() => config);

Hosting your own proxy

Whichever platform you pick, two things stay true everywhere: set the env vars from the Configuration table above as that platform's secrets/environment feature (never commit them), and attach a domain that doesn't hint at the real GitLab host (e.g. badges.yourdomain.tld, not gitlab-proxy-for-<realcompany>.vercel.app) — that's the whole point of this proxy.

On Vercel

Either install the package in your own project, or fork/clone this repo directly and deploy it as-is — both end up running the same app.

As a dependency in your own project:

// api/index.ts
import { handle } from "hono/vercel";
import { app } from "gitlab-badge-proxy";

export const GET = handle(app);
export const runtime = "nodejs";
// vercel.json
{ "rewrites": [{ "source": "/(.*)", "destination": "/api" }] }

Set the env vars in the Vercel project settings (Production + Preview), then vercel deploy --prod.

Forking/cloning this repo instead: it already has api/index.ts and vercel.json set up the same way — vercel link, set the same env vars, vercel deploy --prod, done.

On Cloudflare Workers

// src/index.ts
import { app } from "gitlab-badge-proxy";

export default app; // Hono apps are already a `fetch(request, env, ctx)` handler
# wrangler.toml
name = "gitlab-badge-proxy"
main = "src/index.ts"
compatibility_date = "2024-01-01"

Set GITLAB_URL / GITLAB_PROJECT / etc. with wrangler secret put <NAME> (or in the dashboard under Settings > Variables) rather than plaintext [vars], since GITLAB_TOKEN is a credential. The config module reads process.env, so wire Worker bindings into that shape before calling createApp/loadConfig — e.g. createApp(() => loadConfig(env as unknown as Record<string, string>)) inside your fetch handler, since Workers pass env as a per-request argument rather than a global.

On plain Node.js (bare metal, a VM, or Docker)

// server.mjs
import { serve } from "@hono/node-server";
import { app } from "gitlab-badge-proxy";

serve({ fetch: app.fetch, port: Number(process.env.PORT) || 3000 });
# Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install gitlab-badge-proxy @hono/node-server
COPY server.mjs ./
EXPOSE 3000
CMD ["node", "server.mjs"]

Run with docker run -p 3000:3000 --env-file .env your-image-name, then put a reverse proxy (Caddy/nginx/your platform's own ingress) with TLS in front of it on your domain.

Anywhere else

app is a standard Hono instance, and Hono ships adapters for most other JS runtimes — Deno, Bun, AWS Lambda, Netlify Functions, Fastly Compute — covered in Hono's own deployment docs. The app itself (src/app.ts) has no platform-specific code; only the few lines that call serve()/handle() change per target.

Development

npm install
cp .env.example .env.local   # fill in real values, never commit this file
npm run typecheck
npm test
npm run dev                  # plain Node server on :3000, serves the routes locally
npm run dev:vercel           # optional: vercel dev, if you're testing the Vercel integration specifically

Releasing

CI/CD lives in .gitlab-ci.yml (this repo's origin is GitLab, not GitHub). Every push/MR runs typecheck + test + build on Node 22 and 24. Publishing uses the shared npm-publish CI/CD component — the same one pm2-dash uses — instead of a bespoke publish job: it publishes whenever someone pushes to the deploy-npm branch, relying on the existing prepublishOnly script to typecheck/test/build first and on npm itself refusing to publish over a version that already exists (no separate version check).

One-time setup:

  1. Generate an npm Granular Access Token (https://www.npmjs.com/settings/~/tokens), scoped to this package, read+write, with "bypass 2FA" enabled (CI can't answer an OTP prompt).
  2. GitLab: Settings > CI/CD > Variables → add NPM_TOKEN, flagged Masked + Protected.
  3. GitLab: Settings > Repository > Protected branches → protect deploy-npm (protected variables are only sent to protected branches/tags).

To cut a release:

npm version patch --no-git-tag-version   # or minor / major — bumps package.json only
git commit -am "1.2.3"
git push origin HEAD:deploy-npm

The pipeline publishes automatically once that push lands; watch it under CI/CD > Pipelines.

License

MIT © ArdensLab