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

web-push-lite

v0.1.0

Published

Zero-dependency Web Push (VAPID) sender for Node.js — mints the JWT and encrypts the payload (RFC 8291) with only node:crypto. No native addons, no dependency tree.

Readme

web-push-lite

Send Web Push notifications from Node.js with zero runtime dependencies. It mints the VAPID JWT (ES256) and encrypts the payload per RFC 8291 (aes128gcm) using only the built-in node:crypto module — no native addons, no transitive dependency tree to audit.

import { generateVapidKeys, sendWebPush } from 'web-push-lite';

// One-time: generate a key pair and store it.
const keys = generateVapidKeys();

// Per notification:
const result = await sendWebPush(
  subscription, // the PushSubscription JSON from the browser
  JSON.stringify({ title: 'Job assigned', body: 'Tap to view', url: '/jobs/42' }),
  {
    vapidPublicKey: keys.publicKey,
    vapidPrivateKey: keys.privateKey,
    subject: 'mailto:[email protected]',
  },
);

if (result.expired) {
  // 404/410 — the subscription is gone; delete it from your store.
}

Why

The de-facto library, web-push, pulls in a chain of dependencies (https-proxy-agent, jws, asn1.js, …) for what is, at core, a JWT signature and an ECDH+AES-GCM encryption — both of which Node's standard library already does. web-push-lite is that core, and nothing else:

  • Zero runtime dependencies. Nothing to audit, nothing to get a CVE.
  • Just node:crypto. No node-gyp, no prebuilt binaries.
  • One small surface you can read in a single sitting.

If you send a handful of notification types from your own server, this is likely all you need.

Install

npm install web-push-lite

Requires Node.js 20+ (uses the global fetch and AbortSignal.timeout).

API

generateVapidKeys(): { publicKey, privateKey }

Generate a VAPID application-server key pair, both base64url-encoded. Generate it once, store both halves, and reuse them: the public key is handed to the browser when it calls pushManager.subscribe({ applicationServerKey }) and must stay stable, while the private key signs every push.

const { publicKey, privateKey } = generateVapidKeys();

sendWebPush(subscription, payload, options): Promise<WebPushResult>

Send one notification.

  • subscription — the PushSubscription object serialized by the browser: { endpoint, keys: { p256dh, auth } }.

  • payload — a string or Buffer delivered to your service worker's push event. Pass JSON.stringify(obj) for structured data.

  • options:

    | Field | Required | Default | Notes | |---|---|---|---| | vapidPublicKey | ✓ | — | base64url, from generateVapidKeys() | | vapidPrivateKey | ✓ | — | base64url; keep secret | | subject | ✓ | — | mailto: or https: URL identifying you (VAPID sub claim) | | ttl | | 86400 | seconds the push service retains an undelivered message | | urgency | | 'normal' | 'very-low' \| 'low' \| 'normal' \| 'high' | | topic | | — | later push with the same topic replaces this one | | signal | | 10s timeout | your own AbortSignal |

Returns:

interface WebPushResult {
  statusCode: number; // the push service's HTTP status
  success: boolean;   // 2xx
  expired: boolean;   // 404/410 — subscription gone, delete it
}

sendWebPush only throws on a network/transport error (or your signal aborting). A push service rejecting the message is reported via statusCode/success, not thrown — so a single dead subscription never crashes a fan-out loop.

Fanning out to many subscribers

The library sends one message; you own the loop and your subscription store:

for (const sub of subscriptions) {
  const { expired } = await sendWebPush(sub, payload, opts).catch(() => ({ expired: false }));
  if (expired) await db.deleteSubscription(sub.endpoint);
}

encryptPayload(payload, p256dh, auth): Buffer

The RFC 8291 aes128gcm encryption sendWebPush uses internally, exported for advanced callers who build the HTTP request themselves.

How it works

  1. VAPID JWT — an ES256 (ECDSA P-256 + SHA-256) JWT is signed with your private key, asserting the push endpoint's origin as the audience and your subject as contact. Sent in the Authorization: vapid t=…, k=… header.
  2. Payload encryption — an ephemeral ECDH key agreement with the subscriber's p256dh key derives, via HKDF, a content-encryption key and nonce; the payload is sealed with AES-128-GCM and framed with the aes128gcm content-coding header (salt, record size, and the ephemeral public key).
  3. Delivery — a single POST to subscription.endpoint with the encrypted body and the standard TTL / Urgency / Content-Encoding headers.

Every step is covered by tests that verify against the standard: the JWT is checked with an independent ES256 verification, and the encrypted payload is decrypted back to the original plaintext exactly as a browser's push service would.

Limitations & scope

  • Sends; doesn't store. Managing subscriptions (persisting them, pruning expired ones) is your application's job — see the fan-out example.
  • aes128gcm only. This is the modern encoding every current browser supports. The legacy aesgcm/aesgcm128 encodings are intentionally not implemented.
  • No VAPID key persistence. generateVapidKeys() returns a pair; storing it is up to you.
  • One dependency-free HTTP send. It uses Node's global fetch; there's no built-in retry or proxy support — wrap the call if you need those.

License

MIT © Ian Duncan