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

pingohub-sdk

v1.0.0

Published

Token verification and login URL helpers for tools integrating with Pingohub IAM

Downloads

145

Readme

Pingohub SDK

Helper functions for integrating any internal tool with the Pingohub IAM system. Copy src/index.ts directly into your project — it has zero dependencies and works in any runtime that has the Web Crypto API (Cloudflare Workers, Node.js 18+, Bun, Deno, browsers).


What Pingohub does

Pingohub is a central login gateway. When a user visits a protected tool without a session, the tool redirects them to Pingohub. After the user authenticates, Pingohub checks their access and issues a short-lived HS256 JWT signed with that tool's unique client_secret. The tool then validates the token and creates its own session.

User → tool.example.com (no session)
     → Redirect to Pingohub /token?tool_id=...&redirect_uri=...&state=...
     → User logs in (email/password or Google)
     → Pingohub checks access
     → Redirect to tool.example.com/iam/callback?token=<jwt>&state=<state>
     → Tool calls verifyToken(token, clientSecret)
     → Tool sets its own session cookie
     → User is in

Environment variables your tool needs

Get these from the Pingohub admin dashboard under Tools:

IAM_BASE_URL=https://pingohub.pingolearn.app   # Pingohub deployment URL
IAM_TOOL_ID=<uuid>                              # Your tool's registered ID
IAM_CLIENT_SECRET=<secret>                      # Your tool's signing secret (keep private)

SDK functions

verifyToken(token, clientSecret)

Verifies a JWT issued by Pingohub for your tool. Throws on invalid signature, expiry, or malformed token.

import { verifyToken, type IAMTokenPayload } from './sdk/src/index'

const payload: IAMTokenPayload = await verifyToken(token, IAM_CLIENT_SECRET)
// payload.sub       — user UUID
// payload.email     — user email address
// payload.name      — user display name
// payload.tool_id   — the tool this token was issued for
// payload.iat       — issued-at (Unix seconds)
// payload.exp       — expires-at (Unix seconds, tokens live 1 hour)

Throws:

  • "Invalid token format" — not a three-part JWT
  • "Invalid token signature" — wrong secret or tampered token
  • "Token expired"exp is in the past

buildLoginUrl(iamBaseUrl, toolId, callbackUrl, state?)

Builds the redirect URL to send unauthenticated users to the Pingohub login page.

import { buildLoginUrl } from './sdk/src/index'

const loginUrl = buildLoginUrl(
  'https://pingohub.pingolearn.app',
  IAM_TOOL_ID,
  'https://my-tool.pingolearn.com/iam/callback',
  csrfState  // optional, but recommended
)
// → https://pingohub.pingolearn.app/token?tool_id=...&redirect_uri=...&state=...

Full integration pattern

Cloudflare Worker example

import { verifyToken, buildLoginUrl } from './sdk/src/index'

const IAM_BASE_URL = env.IAM_BASE_URL        // https://pingohub.pingolearn.app
const IAM_TOOL_ID  = env.IAM_TOOL_ID         // your tool's UUID
const IAM_SECRET   = env.IAM_CLIENT_SECRET   // your tool's client_secret
const MY_URL       = 'https://my-tool.pingolearn.com'
const CALLBACK_PATH = '/iam/callback'
const TOKEN_COOKIE  = 'iam_token'

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)

    // 1. Handle the callback from Pingohub after login
    if (url.pathname === CALLBACK_PATH) {
      return handleCallback(request, env)
    }

    // 2. Check for an existing tool session token
    const token = getCookie(request, TOKEN_COOKIE)
    if (token) {
      try {
        const user = await verifyToken(token, IAM_SECRET)
        // Token valid — proceed with the request, attach user to context
        return handleProtectedRequest(request, user)
      } catch {
        // Token expired or invalid — fall through to redirect
      }
    }

    // 3. No valid token — redirect to Pingohub
    const state = crypto.randomUUID()
    // Store state in a short-lived cookie for CSRF verification
    const loginUrl = buildLoginUrl(IAM_BASE_URL, IAM_TOOL_ID, `${MY_URL}${CALLBACK_PATH}`, state)
    return Response.redirect(loginUrl, 302)
  }
}

async function handleCallback(request: Request, env: Env): Promise<Response> {
  const url = new URL(request.url)
  const token = url.searchParams.get('token')
  const state = url.searchParams.get('state')

  if (!token) return new Response('Missing token', { status: 400 })

  // Verify the state matches what you stored (CSRF protection)
  const storedState = getCookie(request, 'iam_state')
  if (state !== storedState) return new Response('Invalid state', { status: 403 })

  // Verify the JWT
  const payload = await verifyToken(token, IAM_SECRET)

  // Set a session cookie with the token (or your own session ID)
  return new Response(null, {
    status: 302,
    headers: {
      Location: '/',
      'Set-Cookie': `${TOKEN_COOKIE}=${token}; HttpOnly; Secure; SameSite=Lax; Max-Age=3600`,
    }
  })
}

function getCookie(request: Request, name: string): string | null {
  const cookies = request.headers.get('Cookie') || ''
  const match = cookies.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
  return match ? decodeURIComponent(match[1]) : null
}

Next.js / Node.js middleware example

// middleware.ts (Next.js)
import { NextRequest, NextResponse } from 'next/server'
import { verifyToken, buildLoginUrl } from '@/sdk'   // adjust path as needed

const IAM_BASE_URL = process.env.IAM_BASE_URL!
const IAM_TOOL_ID  = process.env.IAM_TOOL_ID!
const IAM_SECRET   = process.env.IAM_CLIENT_SECRET!
const CALLBACK_URL = `${process.env.NEXT_PUBLIC_APP_URL}/iam/callback`

export async function middleware(req: NextRequest) {
  // Skip the callback route itself
  if (req.nextUrl.pathname === '/iam/callback') return NextResponse.next()

  const token = req.cookies.get('iam_token')?.value

  if (token) {
    try {
      await verifyToken(token, IAM_SECRET)
      return NextResponse.next()
    } catch { /* fall through */ }
  }

  const state = crypto.randomUUID()
  const res = NextResponse.redirect(buildLoginUrl(IAM_BASE_URL, IAM_TOOL_ID, CALLBACK_URL, state))
  res.cookies.set('iam_state', state, { httpOnly: true, maxAge: 300 })
  return res
}

export const config = { matcher: ['/((?!_next|favicon.ico).*)'] }
// app/iam/callback/route.ts
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { verifyToken } from '@/sdk'

export async function GET(req: Request) {
  const url = new URL(req.url)
  const token = url.searchParams.get('token')!
  const state = url.searchParams.get('state')

  const cookieStore = await cookies()
  if (state !== cookieStore.get('iam_state')?.value) {
    return new Response('Invalid state', { status: 403 })
  }

  await verifyToken(token, process.env.IAM_CLIENT_SECRET!)  // throws if invalid

  cookieStore.set('iam_token', token, { httpOnly: true, secure: true, maxAge: 3600 })
  redirect('/')
}

Alternative: server-side verification via API

If you can't bundle the SDK (e.g. a Python or Go service), call the /verify endpoint instead:

POST https://pingohub.pingolearn.app/verify
Content-Type: application/json

{
  "token": "<jwt from callback>",
  "tool_id": "<your-tool-id>"
}

Success response (200):

{
  "valid": true,
  "user": {
    "sub": "user-uuid",
    "email": "[email protected]",
    "name": "User Name"
  }
}

Failure response (401):

{ "valid": false, "error": "Token expired" }

Note: /verify does not require authentication — it uses the tool_id to look up the tool's secret internally and validate the signature server-side.


Token details

| Field | Value | |---|---| | Algorithm | HS256 (HMAC-SHA256) | | Lifetime | 1 hour (exp - iat = 3600) | | Signed with | Tool's client_secret from the Pingohub dashboard | | Rotation | Rotating the secret in the dashboard immediately invalidates all outstanding tokens for that tool. Users will be transparently re-authenticated on their next request since their Pingohub session is still valid. |


Security checklist

  • Always verify the state parameter in your callback to prevent CSRF attacks.
  • Never expose IAM_CLIENT_SECRET to the browser — keep it server-side only.
  • Store the JWT in an HttpOnly cookie, not localStorage or a JS-accessible cookie.
  • Check payload.tool_id matches your own IAM_TOOL_ID if you verify manually (the SDK does not check this — use the /verify endpoint or add the check yourself if needed).
  • Re-verify on sensitive actions — the token is valid for 1 hour; for write operations you may want to check expiry more aggressively.

Troubleshooting

| Error | Cause | Fix | |---|---|---| | Invalid token signature | Wrong IAM_CLIENT_SECRET or the secret was rotated | Re-fetch the secret from the Pingohub dashboard and update your env var | | Token expired | Token is older than 1 hour | User needs to re-authenticate; redirect to buildLoginUrl(...) | | redirect_uri does not match registered tool URL | Callback URL does not start with the tool's registered URL | Update the tool's URL in the Pingohub admin dashboard to match your callback's origin | | Unknown tool | tool_id is wrong | Copy the exact tool ID from the Pingohub admin dashboard | | User lands on /unauthorized | User has no access grant for this tool | Go to Admin → Teams or Admin → Users and grant access |