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

waverify

v0.1.4

Published

BSP-agnostic WhatsApp OTP delivery and verification, with a Meta AUTHENTICATION template linter, mock simulator, and cost guard. Zero runtime dependencies.

Readme

WaVerify

npm version license node

BSP-agnostic WhatsApp OTP delivery and verification for Node.js — with a Meta AUTHENTICATION template linter, real Cloud API sending, webhook signature verification, SMS fallback, a cost guard, and a CLI. Zero runtime dependencies.

Send and verify a one-time code over WhatsApp behind two calls: start() and verify().

📖 Live Docs & Interactive Playground →

Why

Sending WhatsApp OTPs correctly is deceptively fiddly. Meta forces a fixed, non-editable authentication-template shape, bans URLs/media/emojis, requires a specific OTP button, and expects the code to appear twice in every send payload — get any of it wrong and your message is rejected. On top of that you need secure code handling, single-use verification, attempt caps, spend limits, and a trustworthy way to process delivery webhooks. WaVerify packages all of that so you don't have to rediscover each gotcha yourself.

Install

npm install waverify

Requires Node 18+ (uses built-in fetch and crypto). Zero runtime dependencies.

Quick start

import { WaVerify, CloudApiAdapter, getTemplate } from "waverify";

const wa = new WaVerify({
  adapter: new CloudApiAdapter({
    phoneNumberId: process.env.WA_PHONE_NUMBER_ID!,
    accessToken: process.env.WA_ACCESS_TOKEN!,
  }),
  secret: process.env.WAVERIFY_SECRET!,
});

// 1. Issue + deliver a code over WhatsApp
const { requestId } = await wa.start("+919876543210", getTemplate("copy-code")!);

// 2. Later, check what the user typed
const result = await wa.verify("+919876543210", requestId, userInput);
if (result.valid) {
  // authenticated
}

No credentials yet? Swap CloudApiAdapter for MockAdapter and the whole flow runs in-process with no network, no cost, and deterministic behaviour — ideal for tests and local development.

Features

  • Real WhatsApp sending via Meta's Cloud API (v23.0), with the exact authentication-template payload built for you — including the two quirks that trip everyone up (code appears twice; OTP button sent as sub_type: "url").
  • Template linter — validate an AUTHENTICATION template against Meta's rules before you submit it for approval.
  • Mock simulator — build and test your entire flow with zero network and zero credentials.
  • Webhook verification — constant-time signature checks and clean, typed delivery events.
  • SMS fallback — degrade to SMS when WhatsApp can't deliver, at send time or when a webhook reports failure.
  • Cost guard — per-destination rate limits and a global budget cap stop runaway spend and abuse.
  • CLI — lint templates and preview payloads from the terminal.
  • Zero runtime dependencies — only Node's built-in modules.

Lint a template before you submit it to Meta

import { lintAuthTemplate } from "waverify";

const { ok, issues } = lintAuthTemplate(template);
// issues: [{ level, code, message }, ...]

The linter encodes Meta's authentication-template rules: mandatory OTP button (COPY_CODE / ONE_TAP / ZERO_TAP), one-tap/zero-tap needing an Android package name + signature hash, the expiry range (1–90 minutes), and the URL/media/emoji ban.

Real messages (Meta Cloud API)

CloudApiAdapter sends genuine WhatsApp messages and builds Meta's exact payload for you.

import { WaVerify, CloudApiAdapter } from "waverify";

const wa = new WaVerify({
  adapter: new CloudApiAdapter({
    phoneNumberId: process.env.WA_PHONE_NUMBER_ID!,
    accessToken: process.env.WA_ACCESS_TOKEN!,
    // apiVersion defaults to "v23.0"
  }),
  secret: process.env.WAVERIFY_SECRET!,
});

Getting credentials (free)

  1. Create a Meta app at developers.facebook.com and add the WhatsApp product.
  2. In WhatsApp → API Setup you get a test phone number, a Phone Number ID, and a temporary 24-hour token — enough to message your own verified number immediately.
  3. Create an AUTHENTICATION template in WhatsApp Manager and wait for approval (lint it first).
  4. For production, generate a permanent token via a System User with the whatsapp_business_messaging permission.

CLI

A zero-dependency CLI, available via npx:

npx waverify templates                      # list built-in template presets
npx waverify template copy-code             # print a preset as JSON
npx waverify template copy-code > otp.json  # save it to customize
npx waverify lint otp.json                  # validate against Meta's rules (exit 0/1)
npx waverify preview otp.json --code 483920 # print the Cloud API send payload

lint exits non-zero on errors, so it drops straight into CI to catch a bad template before you submit it to Meta.

Built-in templates

The same presets are available in code:

import { getTemplate, listTemplates } from "waverify";

listTemplates();                 // ["copy-code", "copy-code-minimal", "one-tap", "hindi"]
const template = getTemplate("copy-code");  // an independent, mutable copy

Every preset passes lintAuthTemplate. Rename name to match the template you register in WhatsApp Manager, and for one-tap fill in your real Android packageName and signatureHash.

Delivery receipts & webhook verification

Meta POSTs delivery/read/failure updates to a webhook you host. WaVerify verifies those requests and hands you clean, typed status events.

Next.js App Router (no extra dependencies)

// app/api/whatsapp/webhook/route.ts
import { createWebhookHandler } from "waverify";

const handler = createWebhookHandler({
  verifyToken: process.env.WA_VERIFY_TOKEN!,
  appSecret: process.env.WA_APP_SECRET!,
  onStatus: (e) => {
    // e.status is "sent" | "delivered" | "read" | "failed"
    if (e.status === "failed") {
      console.error("Undeliverable:", e.messageId, e.errors);
    }
  },
});

export const GET = (req: Request) => handler(req);   // handshake
export const POST = (req: Request) => handler(req);  // signed events

The handler is built on the Web-standard Request/Response API, so it also works unchanged in any Fetch-based runtime (edge functions, Hono, Bun, Deno).

Express (use the pure helpers)

Keep the raw body — signature verification fails against re-serialized JSON:

import express from "express";
import { verifySignature, parseStatusEvents } from "waverify";

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifySignature({
    rawBody: req.body, // Buffer, untouched
    signatureHeader: req.headers["x-hub-signature-256"] as string,
    appSecret: process.env.WA_APP_SECRET!,
  });
  if (!ok) return res.sendStatus(401);

  for (const e of parseStatusEvents(JSON.parse(req.body.toString("utf8")))) {
    // handle e
  }
  res.sendStatus(200);
});

Security note: signatures are HMAC-SHA256 over the raw request bytes with your App Secret. WaVerify's helpers only accept the raw body and compare in constant time, so the most common webhook vulnerability — verifying a parsed-then-re-stringified body, or skipping verification entirely — can't happen.

SMS fallback

Give WaVerify an SMS adapter and it degrades gracefully when WhatsApp can't deliver.

import { WaVerify, CloudApiAdapter, MockSmsAdapter } from "waverify";

const wa = new WaVerify({
  adapter: new CloudApiAdapter({ /* ... */ }),
  smsAdapter: new MockSmsAdapter(),   // bring your own SMS provider
  smsText: "{{code}} is your verification code.",
  secret: process.env.WAVERIFY_SECRET!,
});

const res = await wa.start("+919876543210", template);
if (res.usedFallback) {
  // WhatsApp failed at send time; the SAME code went out over SMS instead.
}

Two fallback moments, both handled:

  • Synchronous — if the WhatsApp send throws, start() retries over SMS with the same code and sets usedFallback: true. Set smsFallback: false to opt out.
  • Asynchronous — if Meta accepts the message but a webhook later reports failed (e.g. the recipient has no WhatsApp), call resendViaSms() from your onStatus handler. It issues a fresh code (plaintext is never stored, so the original can't be re-sent) and refreshes the challenge.

Bring any SMS provider by implementing the one-method SmsAdapter interface. MockSmsAdapter ships for tests and simulation; a first-party Twilio SMS adapter is on the roadmap.

Architecture

Every piece is an interface with an in-the-box default, so each is swappable:

| Concern | Contract | Default | |--------------------|-------------------|----------------------| | Delivery | WhatsAppAdapter | MockAdapter | | SMS fallback | SmsAdapter | MockSmsAdapter | | Challenge storage | OtpStore | InMemoryOtpStore | | Spend / abuse | CostGuard | sane in-memory caps |

Codes are generated with a CSPRNG and stored only as an HMAC — plaintext never touches the store. Verification is single-use, time-boxed, attempt-capped, and uses constant-time comparison.

Development

These scripts are for working on WaVerify itself (after cloning the repo):

npm test       # node:test unit suite (no credentials needed — fetch is mocked)
npm run smoke  # end-to-end pipeline through the mock adapter
npm run build  # emit dist/

Roadmap

Shipping today: OTP engine, template linter, mock simulator, Meta Cloud API adapter, webhook signature verification, SMS fallback, cost guard, built-in templates, and CLI.

Planned:

  • First-party Twilio adapter (WhatsApp + SMS)
  • Shared Postgres/Redis store and cost guard for multi-instance deployments
  • Hardening: request timeouts, retry-on-transient, and webhook event de-duplication

License

MIT