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

@agentwares/web-bot-auth

v0.1.1

Published

Web Bot Auth for agents: Ed25519 keys, a signed /.well-known/http-message-signatures-directory, and RFC 9421 HTTP Message Signatures on outbound requests, so Cloudflare can verify your bot. Web Crypto only, no Node built-ins.

Readme

@agentwares/web-bot-auth

Sign your agent's outbound HTTP requests with Ed25519 so a verifier can tell who is calling, and serve the signed key directory that publishes the key. This is the mechanism behind Cloudflare's Verified Bots programme: a fetcher that signs is identified, and a fetcher that does not is guessed at from its IP and user agent.

Web Crypto and the Fetch API only — no Node built-ins on the signing path — so it runs unchanged on Vercel Functions, Cloudflare Workers, Deno and Node 20+.

npm install @agentwares/web-bot-auth

Sign a request

import { importSigningKey, signRequest, attachSignature } from "@agentwares/web-bot-auth";

const key = await importSigningKey(JSON.parse(process.env.WEB_BOT_AUTH_PRIVATE_JWK!));

const request = new Request("https://example.com/page");
const headers = await signRequest(request, key, { directory: "https://bot.example" });

const response = await fetch(attachSignature(request, headers));

headers is the three fields a signed request carries:

Signature-Agent: "https://bot.example"
Signature-Input: sig1=("@authority" "signature-agent");created=1757520000
 ;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519"
 ;expires=1757520060;nonce="…";tag="web-bot-auth"
Signature: sig1=:…:

The default window is 60 seconds, which is Cloudflare's recommended bound on replay. Cover more than the authority when you can — alsoCover: [{ name: "@method" }, { name: "@path" }] narrows a signature that would otherwise be reusable against any path on that origin until it expires.

Serve the key directory

The directory is a JWKS at a fixed path, and it signs itself: one signature per published key, covering the @authority of the request that fetched it. That possession proof is what stops someone re-serving your key set under their domain and registering as you.

import { directoryHandler } from "@agentwares/web-bot-auth";

export const GET = directoryHandler([key]); // mount at the well-known path

It answers with Content-Type: application/http-message-signatures-directory+json, a Content-Digest, and Signature / Signature-Input tagged http-message-signatures-directory. It must be generated per request, because @authority comes from the request.

The URL you register with Cloudflare is the origin plus the well-known path, over HTTPS, with no query and no redirect:

https://<your-host>/.well-known/http-message-signatures-directory

directoryUrl("https://bot.example") builds it. The Signature-Agent header carries the origin ("https://bot.example"); the verifier appends the well-known path itself.

Verify

import { verifyRequest, verifyDirectoryResponse } from "@agentwares/web-bot-auth";

const result = await verifyRequest(request, { keys: publishedJwks });
if (result.ok) console.log(result.keyid, result.signatureAgent);
else console.log(result.code, result.cause, result.fix);

verifyRequest never throws on hostile input — a malformed field, an unusable key or a replayed signature all come back as a code / cause / fix failure. It rebuilds the signature base from the message it actually received, so a signature only passes if every covered component survived the trip byte for byte.

verifyDirectoryResponse(request, response) checks a directory's possession proofs and returns which published thumbprints proved possession.

Generate a key

pnpm --filter @agentwares/web-bot-auth build
pnpm --filter @agentwares/web-bot-auth keygen

Writes the private JWK to ~/code/agentwares-secrets/web-bot-auth.key at mode 0600, the public JWK and the thumbprint beside it, and prints only the thumbprint. It refuses to overwrite an existing key, because a key registered with Cloudflare cannot be silently replaced — rotation means publishing both and retiring the old one.

The private key never goes in an environment variable, a commit, or a log. Deployments need the public JWK, which is served to the entire internet anyway.

Which spec this implements

Built from, and tested against, the published test vectors in:

  • RFC 9421, HTTP Message Signatures (February 2024) — signature base construction, @signature-params, structured-field serialization.
  • draft-ietf-webbotauth-httpsig-protocol-00 (1 September 2026) — the Web Bot Auth working-group draft: Signature-Agent, the web-bot-auth tag, the JWKS directory format, the well-known URI, and the Appendix B directory possession proof. It is a draft; it expires 5 March 2027 and the header syntax has already changed once (see below).
  • RFC 7638 / RFC 8037 Appendix A.3 — the JWK thumbprint used as keyid.
  • Cloudflare's Web Bot Auth documentation, which is what actually gates the Verified Bots programme today.

The Ed25519 vectors from Appendix E.2 of the draft and Appendix B.2.6 of RFC 9421 are asserted byte for byte in the test suite: Ed25519 is deterministic, so a correct implementation reproduces the published signature exactly, not merely one that verifies. signRequest reproduces the example in Cloudflare's own documentation on the nose.

Where Cloudflare and the current draft disagree

Cloudflare implements the older draft-meunier-http-message-signatures-directory-03 / -web-bot-auth-architecture-02. Two things have changed since, and the defaults here follow Cloudflare, because Cloudflare is the verifier that decides whether your bot gets through.

| | Cloudflare today (the default) | draft-ietf-webbotauth-httpsig-protocol-00 | | -------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | Signature-Agent | sf-string: "https://bot.example", covered as "signature-agent" | Dictionary: sig1="https://bot.example", covered as "signature-agent";key="sig1" | | Directory signature covers | ("@authority";req) | ("@authority";req "content-digest") |

Cloudflare's docs are explicit that it fails verification for the dictionary form. Pass signatureAgentForm: "dictionary" and profile: "ietf-draft" when you are talking to a verifier that has moved on. Both forms are covered by the vectors and by round-trip tests.

Cloudflare also rejects the sf, bs, key and req component parameters on request signatures, and the @query-param and @status components. This library refuses sf and bs outright; the rest are available and simply should not be sent to Cloudflare.

Re-check all of this before you deploy. It moved between this package's first and second paragraph of research, and it will move again.

License

MIT