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

@vibedout/webhooks

v0.2.1

Published

Webhooks in the Standard Webhooks format, sent and received, for Workers, browsers and Node: signing and verifying (webhook-id, webhook-timestamp, webhook-signature v1 with a rotation window), the event envelope, the 24-hour retry schedule, a target check

Downloads

287

Readme

@vibedout/webhooks

Outgoing webhooks in the Standard Webhooks format, written once: signing and verifying, the event envelope, the 24-hour retry schedule, a check on where a webhook may be sent, and one delivery call that says what the answer means. WebCrypto only; @vibedout/web-crypto and @vibedout/provider-kit are bundled, so there is no runtime dependency.

It is the core of the connectors feature (templates/_shared/blocks/connectors): the feature stores endpoints and runs the queue, this package is what must not drift. Anything that receives these webhooks (the n8n node, a Zapier app, your own server) can use verify from here, or the same ten lines in any language: see "Receiving".

import { deliver, deriveSecret, buildEnvelope, serializeEnvelope, retryDelaySeconds, matchesEvent } from "@vibedout/webhooks";

Modules

| Module | What it is | |---|---| | secret | whsec_ + base64 of 24 to 64 bytes. generateSecret(), secretBytes(), and deriveSecret(master, endpointId, version): a secret computed from one server secret, so none is stored per endpoint and rotating is bumping the version | | sign | sign, signatureHeader (several secrets, newest first, space separated: a rotation window), signedHeaders, and verify for a receiver (signature first, then the clock) | | envelope | { id, type, timestamp, subject, data } and the exact text that is signed (serializeEnvelope) | | schedule | retryDelaySeconds(attemptsMade): ten attempts over 24 hours, up to a tenth of each wait taken off at random | | patterns | *, an exact type or domain.*; matchesEvent(patterns, type) | | target | checkTarget(url): https only, no credentials, no loopback, private, link-local or reserved address in any spelling, no .local/.internal name, no host of your own | | inbound | Receiving (50.16-T28): InboundSource (what a feature adds to the webhooks-in route: it verifies, claims and applies one delivery), the registry the generated barrel registers sources in (registerWebhookSource, read with webhookSources() inside a request), WebhookRefused (a delivery not to be trusted: status and reason), readCapped (a body read with a ceiling), InboundContext.atomic (the host's claims written in the same D1 batch as a delivery's effects, so a Worker that dies between a claim and its effect loses nothing: BatchedD1ReceiptStore of @vibedout/web-crypto, which the generic sources use and hand to their handle) and two generic sources: standardWebhooksSource (the format above) and hmacSource (a plain HMAC-SHA256 of the body, or of timestamp.body, with its header, prefix, encodings and id header chosen by the caller; a time header is allowed only with timestamp.body, because a time the signature does not cover proves nothing and the call throws). The registry lives on the isolate's global object under Symbol.for("@vibedout/webhooks/inbound.sources"), so two copies of the package in one Worker share it | | deliver | deliver({ url, id, timestamp, body, secrets }): one signed POST with a timeout, no redirects, 2 KiB of the answer kept; the outcome is delivered (2xx), gone (410), retry (anything else) or blocked (the address is not allowed) |

The wire format

POST <endpoint url>
content-type: application/json
user-agent: vibedout-webhooks/1
webhook-id: 01K7...                       the event's id; the same on every retry and replay
webhook-timestamp: 1760000000             whole seconds, of THIS attempt
webhook-signature: v1,<base64> v1,<base64>   HMAC-SHA256 of "{id}.{timestamp}.{body}"; two while a secret is being rotated

{"id":"01K7...","type":"order.paid","timestamp":"2026-10-15T12:00:00.000Z","subject":{"type":"order","id":"..."},"data":{...}}

Any 2xx is a success. 410 Gone disables the endpoint (nothing is retried). Any other answer, a timeout or a refused connection is retried at these moments after the first attempt: at once, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h and 24 h. After the tenth the delivery has failed. A receiver should answer quickly with a 2xx and work afterwards, and dedupe on webhook-id.

Receiving

import { verify } from "@vibedout/webhooks/sign";

const body = await request.text(); // the raw text: never JSON that was parsed and written again
const result = await verify(request.headers, body, { secrets: process.env.WEBHOOK_SECRET });
if (!result.ok) return new Response("bad signature", { status: 400 }); // result.reason: no_match, expired, ...

In another language: HMAC-SHA256 over "{webhook-id}.{webhook-timestamp}.{body}" keyed by the base64-decoded part of the secret (after whsec_), base64 of the result, compared in constant time with each v1, value of the header, and a timestamp within five minutes.

Where it is tested

pnpm --filter @vibedout/webhooks test (80 tests): the signature of the Standard Webhooks documentation example, Node's own HMAC as an independent oracle, every spelling of a private address, the schedule's sums, and deliver against a recorded fetch (a 500, a 410, a redirect, a timeout, a body that never ends), and inbound (the signature before the clock, a replay under a fresh id, a refusal that releases the claim, the atomic claim a source uses when the host has one, a body past its cap, the registry shared by two copies of the module).