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

@paysmith/webhook

v0.0.1

Published

Verify Paysmith signed webhook events from raw request bytes — Ed25519 with timestamp tolerance and pinned key ids — and deduplicate at-least-once deliveries.

Readme

@paysmith/webhook

Receiver-side verification for signed Paysmith events. @paysmith/webhook checks a delivery's Ed25519 signature against the exact raw request bytes and a timestamp tolerance, parses the result into a typed paysmith.event/v1 envelope, and deduplicates deliveries by event_id through a pluggable store. This is the enforcement point for the one rule Paysmith is built around: content unlocks only after a signed event verifies — never because a return URL or a client claims success.

Paysmith deliveries are at-least-once. Every webhook handler built on this package must be idempotent: the deduper exists specifically so a re-delivered event (Paysmith's sandbox has a duplicate_webhook demo scenario that deliberately redelivers) can never grant, revoke, or record anything twice.

Install

pnpm add @paysmith/webhook
# or
npm i @paysmith/webhook

@paysmith/contracts is a dependency and is installed automatically.

Quick start: a webhook handler (Node / Next.js route)

// app/api/paysmith/webhook/route.ts
import { verifyPaysmithEvent, createEventDeduper, createJsonFileDeduperStore } from "@paysmith/webhook";
import { NextResponse, type NextRequest } from "next/server";

const deduper = createEventDeduper(createJsonFileDeduperStore(".paysmith/seen-events.json"));
// The public key for the sandbox's pinned `public_key_id` — fetched once and
// cached by your app, e.g. via `@paysmith/sdk/server`'s `getSandboxPublicKey()`.
declare const PINNED_SANDBOX_PUBLIC_KEY_PEM: string;

export async function POST(request: NextRequest): Promise<NextResponse> {
  // Verification covers the exact raw bytes — read text before any JSON.parse.
  const rawBody = await request.text();

  const result = verifyPaysmithEvent({
    rawBody,
    headers: request.headers,
    publicKeyPem: PINNED_SANDBOX_PUBLIC_KEY_PEM, // pin by key id — see "Key pinning" below
  });

  if (!result.ok) {
    // result.reason: "missing_headers" | "invalid_timestamp" | "invalid_environment"
    //   | "timestamp_out_of_tolerance" | "malformed_signature" | "signature_mismatch"
    //   | "invalid_json" | "invalid_event_envelope"
    return NextResponse.json({ error: "invalid_signature", reason: result.reason }, { status: 400 });
  }

  const { event } = result; // typed EventEnvelope
  const isFirstDelivery = await deduper.claim(event.event_id);
  if (!isFirstDelivery) {
    return NextResponse.json({ received: true, deduplicated: true });
  }

  if (event.type === "payment.succeeded") {
    // Grant the entitlement here — this is the only place in the app that may.
  } else if (event.type === "payment.refunded") {
    // Revoke it.
  }

  return NextResponse.json({ received: true });
}

The rule this enforces: unlock only after verifyPaysmithEvent returns { ok: true }, and only once per event_id. Never grant access from a checkout confirm response or a return-URL redirect — those are UI hints, not proof.

API

verifyPaysmithEvent(input): VerifyPaysmithEventResult

interface VerifyPaysmithEventInput {
  rawBody: string | Uint8Array;
  headers: Pick<Headers, "get"> | Readonly<Record<string, string | readonly string[] | undefined>>;
  publicKeyPem: string;
  toleranceSeconds?: number; // defaults to 300 (5 minutes)
  nowSeconds?: number;       // injectable clock, mainly for tests
}

type VerifyPaysmithEventResult =
  | { ok: true; event: EventEnvelope }
  | { ok: false; reason: VerifyPaysmithEventFailureReason };

headers accepts either a Fetch Headers instance or a plain record (e.g. Node's IncomingHttpHeaders) — lookups are case-insensitive either way. The function never throws: every failure mode, from a malformed header to a stale timestamp to a bad signature to a schema mismatch, comes back as { ok: false, reason }.

Timestamp tolerance. The signed timestamp must be within toleranceSeconds (default 300) of nowSeconds (default: the real clock). The window is checked before the signature so a stale-but- otherwise-valid delivery is reported as timestamp_out_of_tolerance rather than a generic mismatch.

Key pinning. verifyPaysmithEvent verifies against whatever publicKeyPem you pass it — it does not fetch or cache keys itself. Your handler is responsible for pinning: only ever supply the public key for the public_key_id your sandbox was activated with, and refuse deliveries whose paysmith-key-id header doesn't match that pinned id. Never re-fetch a key just because a signature failed to verify — that turns every forged signature into a free key lookup for an attacker.

createEventDeduper(store): EventDeduper

interface EventDeduper {
  claim(eventId: string): Promise<boolean>; // true = first time seen; false = already claimed
}

Concurrent claim() calls for the same eventId are serialized, so two deliveries racing in can never both observe themselves as "first."

Deduper stores

function createMemoryDeduperStore(): EventDeduperStore;       // in-process only; resets on restart
function createJsonFileDeduperStore(filePath: string): EventDeduperStore; // persists across restarts

createJsonFileDeduperStore writes to a sibling temp file and publishes with a single rename, so a crash mid-write never leaves a truncated store on disk. Implement EventDeduperStore (has / add) directly to back the dedupe set with your own database.

readHeader(headers, name): string | undefined

Case-insensitive header lookup across either header shape verifyPaysmithEvent accepts — exported for callers building their own diagnostics around a delivery.

How it fits

@paysmith/webhook is where the signed event becomes an entitlement: it's the only step between a payment intent settling and an application deciding to unlock something. A receipt is retrievable afterward as proof of what happened, but the unlock decision itself is made here, once, per event_id.

License

MIT