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

@enruta/accept

v0.2.0

Published

Accept middleware for merchants: detects agent traffic, verifies Web Bot Auth signatures against the agent's key directory (Ed25519) and UCP signatures against the agent's UCP profile (ES256 with Content-Digest), verifies Enruta mandates (ES256), labels t

Readme

@enruta/accept

The Accept middleware for merchants. It sits in front of a checkout and answers one question for every order an agent places: which agent placed it, on behalf of whom, under which limit and which policy — and it puts that answer on the request as headers and, when the order is placed, into an Enruta record.

Web APIs only (WebCrypto Ed25519 and ES256, fetch, Request/Response), no runtime dependencies. The same code runs on Cloudflare Workers, Vercel Edge, Fastly Compute, Deno and Node 22.

npm install @enruta/accept

What it does, per request

| Step | What happens | Where it shows up | | --- | --- | --- | | Detect | Reads UCP-Agent: profile="…", MCP JSON-RPC bodies (meta["ucp-agent"]), RFC 9421 Web Bot Auth (Signature-Agent, Signature-Input, Signature) and known agent User-Agent tokens; resolves the platform through a fingerprint table (Google, ChatGPT/OpenAI, Copilot, Meta Muse, RyeBot, Enruta, Anthropic, Perplexity, Amazon). | X-Enruta-Agent-Platform, X-Enruta-Agent-Kind | | Identify | On a signed request, fetches the Signature-Agent origin's key directory (/.well-known/http-message-signatures-directory), picks the key whose JWK SHA-256 thumbprint is keyid, and verifies the RFC 9421 signature. A UCP signature (the scheme of Shopify's "Signed" traffic tier) verifies with the key the UCP-Agent profile publishes under keyid: ES256 over the components it declares, with Content-Digest checked against the raw body. Visa Trusted Agent Protocol signatures verify against a trusted directory you configure. See Web Bot Auth and UCP signatures. | X-Enruta-Agent-Verified, X-Enruta-Signature-Agent, X-Enruta-Agent-Profile, risk_signals | | Verify | On POST …/checkout-sessions/{id}/complete and MCP tools/call complete_checkout, reads ap2.checkout_mandate, verifies the ES256 signature with the keys of the Enruta platform profile (cached; /.well-known/enruta-keys.json as fallback), checks exp, payee === domain and checkout.reference === {id}. | X-Enruta-Mandate-Valid: true|false | | Label | Puts the verified mandate's agent, mandate id, policy, principal on the forwarded request. Any X-Enruta-* header the client sent is removed first. | X-Enruta-Agent, X-Enruta-Mandate, X-Enruta-Policy, X-Enruta-Policy-Version, X-Enruta-Principal, X-Enruta-Principal-Login | | Report | When the merchant answers 2xx with an order, posts { domain, order, mandate_jws, checkout_id, source: "middleware", risk_signals? } to POST /v0/sellers/orders with the seller key. In the background through waitUntil when the runtime has one, otherwise awaited with a 3 s timeout; failures are events, never errors for the buyer. | X-Enruta-Record: rec_… on the response when the report finished synchronously |

An invalid or missing mandate is passed through with X-Enruta-Mandate-Valid: false: the merchant's own AP2 check decides. Only with enforce: true does the middleware answer itself (402 and the AP2 error envelope: mandate_required, agent_missing_key, mandate_invalid_signature, mandate_expired, mandate_scope_mismatch; an MCP call gets the same envelope in band as a tool error). Likewise a signature that verifies under neither Web Bot Auth nor UCP is only labelled, unless requireVerifiedBotSignature is set (403 with bot_signature_invalid or bot_signature_required; a verified signature of either scheme passes).

What never leaves the merchant: the credential token (payment.instruments[].credential) is not read into labels or events, not logged, and stripped from the order before the report; the request body reaches the merchant untouched (the middleware parses a clone).

Headers

Request headers set on the forwarded request:

| Header | Value | When | | --- | --- | --- | | X-Enruta-Agent-Platform | google, openai, copilot, muse, rye, enruta, anthropic, perplexity, amazon, or none | always (a hint: it can come from an unverified header) | | X-Enruta-Agent-Kind | ucp, mcp, web_bot_auth, user_agent, none | always | | X-Enruta-Agent-Verified | true when the Web Bot Auth, Visa TAP or UCP signature verified, else false | signed requests | | X-Enruta-Signature-Agent | the verified agent origin, e.g. https://agent.example | the Web Bot Auth or Visa TAP signature verified | | X-Enruta-Agent-Profile | the verified UCP profile URL, e.g. https://platform.example/.well-known/ucp | the UCP signature verified | | X-Enruta-Mandate-Valid | true / false | complete requests | | X-Enruta-Agent | agt_… | mandate signature verified | | X-Enruta-Mandate | mnd_… | mandate signature verified | | X-Enruta-Principal | the buyer organization id (sub) | mandate signature verified | | X-Enruta-Policy, X-Enruta-Policy-Version | policy id and version | when the mandate carries policy | | X-Enruta-Principal-Login | principal_login_id | when the mandate carries it |

Response header: X-Enruta-Record: <seller_record_id> when the report finished before the response was sent.

The names are exported as HEADERS (HEADERS.agent, HEADERS.mandate, HEADERS.agentVerified, HEADERS.signatureAgent, HEADERS.agentProfile, …).

Web Bot Auth

Follows draft-ietf-webbotauth-httpsig-protocol-00 (September 2026), which replaced draft-meunier-web-bot-auth-architecture and draft-meunier-http-message-signatures-directory.

A signature counts as verified when all of these hold:

  1. Signature-Input and Signature parse (RFC 8941) and share the label.
  2. tag="web-bot-auth"; created, expires and keyid are present; @authority or @target-uri is covered.
  3. The signature covers a Signature-Agent member (the dictionary member keyed by its label, else the single member it covers; the legacy string form when it covers the bare field). The member is an https origin (type directory, the default), not an IP literal or a local name, and it is allowed by allow / not on deny.
  4. created ≤ now + 60 s, expires ≥ now − 60 s, expires − created ≤ 24 h; alg, when present, is ed25519 (or another configured algorithm) and matches the key.
  5. The key comes from that origin's directory, fetched over HTTPS with a 200 (no redirects, 3 s timeout, 64 KiB, 64 keys), and its RFC 7638 thumbprint equals keyid. The key has not passed its exp, and is not an RFC 9421 example key. If the directory response carries http-message-signatures-directory signatures (Appendix B), the key needs a valid one: @authority;req plus content-digest, the digest matching the body, not dated in the future.
  6. The RFC 9421 signature verifies over the components the request declares (@method, @path, @query, @query-param, header fields, dictionary members with key, …).
  7. A nonce, when present, has not been seen within the signature's lifetime (in-memory per instance, or your nonceStore).

Anything else is not verified, with status and reason:

| status | reason | | --- | --- | | invalid | malformed, missing_parameter, missing_component, signature_agent_missing, signature_agent_invalid, unsupported_alg, not_yet_valid, expired, validity_too_long, nonce_replayed, directory_signature_invalid, key_expired, test_key, invalid_signature | | unverified | unknown_key, directory_unavailable, directory_signature_missing, unsupported_discovery_type, agent_not_allowed, agent_denied, tag_mismatch, not_checked (parse only) | | unsigned | — |

Directories are cached per origin for Cache-Control: max-age (at least 60 s, 1 h when absent, at most 24 h). A failed fetch is remembered for 30 s and never evicts a cached copy. An unknown keyid refetches a fresh directory at most once a minute, so rotated keys are picked up. Concurrent fetches of one directory are coalesced.

Visa Trusted Agent Protocol (webBotAuth.visaTap): signatures tagged agent-browser-auth or agent-payer-auth cover @authority and @path. They carry created, expires (at most 8 minutes apart), keyid, alg and nonce, and verify with a key from the trusted directory (kid or thumbprint). true uses Visa's published https://mcp.visa.com/.well-known/jwks. As of September 2026 that key store serves one RSA key from a sandbox CA and no agent signing key, so point directoryUrl at the key store your Visa onboarding gives you.

UCP signatures

Follows the UCP specification (2026-08-25): Message Signatures and the Identity Resolution Algorithm of the overview. It is the scheme of Shopify's "Signed" traffic tier: the agent signs its requests with HTTP Message Signatures (RFC 9421, ECDSA P-256) and publishes the public key in its UCP profile. Requests signed the way the UCP reference server signs them verify here, and signatures made by this package's tests verify with the reference server's check; the tests cross-check both directions.

A signature counts as verified when all of these hold:

  1. Signature-Input and Signature parse (RFC 8941) and share the label. The signature has no tag, or tag="web-bot-auth" (the dual-audience shape); signatures with other tags are left to other verifiers.
  2. UCP-Agent parses as a dictionary whose profile is an https URL of at most 1024 characters on a public host: not an IP literal, not a local name (localhost, single-label, .local, .internal, .home.arpa), no credentials.
  3. keyid is present (at most 512 characters). created, when present, is ≤ now + 60 s, and expires, when present, is ≥ now − 60 s. Neither is required: UCP leaves replay protection to the signed Idempotency-Key.
  4. The signature covers @method, @authority and @path; @query when the URL has a query string; content-digest and content-type when the request has a body; and idempotency-key, ucp-agent and signature-agent when the request carries them (signature-agent as the whole field or as the member under the signature's label).
  5. Content-Digest has a sha-256 member, and every digest it carries matches the raw body bytes. It is checked whenever the body is not empty or the digest is covered, so a stripped body does not pass. A body over maxBodyBytes is not verified.
  6. The profile comes back with a 2xx (no redirects, 3 s timeout, 128 KiB, 64 keys) and publishes a key whose kid is keyid, in keys[] or signing_keys[], at the top level or under ucp. Keys with a use other than sig, or with key_ops that leave out verify, are skipped.
  7. The key is EC P-256 (ES256) or Ed25519 (EdDSA), and its alg, when present, matches its curve. The signature's alg parameter, when present, names that algorithm (ecdsa-p256-sha256, ed25519). A web-bot-auth tagged signature's keyid is the key's RFC 7638 thumbprint.
  8. The signature verifies over the components the request declares: ES256 as 64 bytes of raw r||s (DER is refused), Ed25519 as 64 bytes. When a request carries several signatures, one that verifies is enough.

Anything else is not verified, with status and reason:

| status | reason | | --- | --- | | invalid | malformed, ucp_agent_invalid, missing_parameter, missing_component, not_yet_valid, expired, digest_mismatch, unsupported_alg, invalid_signature | | unverified | ucp_agent_missing, profile_unavailable, unknown_key, body_unavailable, tag_mismatch, not_checked (parse only) | | unsigned | — |

The result is agent.ucp_signature, next to agent.web_bot_auth:

ucp_signature: {
  signed, verified,
  status,                    // "verified" | "invalid" | "unverified" | "unsigned"
  profile_url, keyid,
  alg,                       // "ES256" | "EdDSA", from the key
  created, expires,
  reason?, detail?           // why it did not verify
}

agent.verified is true when either web_bot_auth or ucp_signature verified. A verified UCP signature sets X-Enruta-Agent-Profile and adds { "type": "ucp_signature_verified", "profile_url", "keyid", "alg" } to risk_signals, which the order report carries.

Profiles are cached per URL for Cache-Control: max-age (at least 60 s, 1 h when absent, at most 24 h). A failed fetch is remembered for 30 s. An expired copy is not used when its refresh fails, so a key removed from the profile stops verifying once the cached copy expires. An unknown keyid refetches the profile at most once a minute per origin, so rotated keys are picked up. Concurrent fetches of one profile are coalesced.

Deploy

Cloudflare Worker (in front of any store)

import { createAccept, toFetchHandler } from "@enruta/accept";

let handler: ReturnType<typeof toFetchHandler> | undefined;

export default {
  fetch(request: Request, env: { ENRUTA_SELLER_KEY?: string }, ctx: ExecutionContext) {
    handler ??= toFetchHandler(
      createAccept({ domain: "store.example.com", sellerKey: env.ENRUTA_SELLER_KEY }),
      { origin: "https://origin.store.example.com" },
    );
    return handler(request, env, ctx);
  },
};

The request is proxied to origin with the labels, X-Forwarded-Host and X-Forwarded-Proto; the report runs in ctx.waitUntil. The same handler is a Vercel Edge Function (export default toFetchHandler(accept, { origin }); the edge context with waitUntil is the second argument) and a Fastly Compute or Deno handler.

Next 16 proxy.ts (Vercel)

import { NextResponse, type NextRequest } from "next/server";
import { createAccept, toNextProxy } from "@enruta/accept";

const accept = createAccept({ domain: process.env.STORE_DOMAIN!, sellerKey: process.env.ENRUTA_SELLER_KEY });
const enruta = toNextProxy(accept, { next: (init) => NextResponse.next(init) });

export async function proxy(request: NextRequest) {
  return enruta(request);
}

export const config = { matcher: ["/checkout-sessions/:id/complete", "/mcp"] };

The route handlers read the labels from the request headers (request.headers.get("x-enruta-agent")). A proxy never sees the route's response, so nothing is reported from proxy.ts; the store's own order webhook to Enruta covers the record, or call accept.report({ order, mandate_jws, checkout_id }) from the route. To combine with an existing proxy, call enruta(request) for the two paths and keep your other rewrites. nextHeadersFor(inspection, request.headers) gives the header set for NextResponse.next({ request: { headers } }) when you want to run accept.inspect yourself.

Hono (Workers, Node, Deno, Bun)

import { Hono } from "hono";
import { createAccept, toHonoMiddleware } from "@enruta/accept";

const accept = createAccept({ domain: "store.example.com", sellerKey: process.env.ENRUTA_SELLER_KEY });
const app = new Hono();
app.use("/checkout-sessions/:id/complete", toHonoMiddleware(accept));
app.use("/mcp", toHonoMiddleware(accept));

The handler sees the labels on c.req.raw.headers and the full inspection as c.get("enruta"); the middleware sees the response, so the report runs here too (through c.executionCtx.waitUntil on Workers).

Express (Node)

app.use(express.json());
app.use(toExpress(accept)); // labels in req.headers, inspection in req.enruta; call accept.report(...) once the order is placed

A UCP signature binds the raw body bytes (Content-Digest), and express.json() hands the middleware a re-serialized body that no longer matches. When agents sign their requests, mount express.raw({ type: "application/json" }) in front of toExpress instead, and parse the JSON in the route.

API

createAccept(options: AcceptOptions): Accept

| Option | Default | Meaning | | --- | --- | --- | | domain | required | the merchant domain a mandate names as payee | | enrutaApiUrl | https://api.enruta.ai | report endpoint and JWKS fallback | | platformProfileUrl | https://app.enruta.ai/.well-known/agent-profile.json | where the mandate keys come from | | sellerKey | — | seller API key; without it the middleware labels but does not report | | report | true when sellerKey is set | report placed orders | | enforce | false | answer complete requests without a valid mandate with the AP2 error | | webBotAuth | on | Web Bot Auth / Visa TAP verification options (below); false only parses the signature headers | | ucpSignature | on | UCP signature verification options (below); false only parses the signature headers | | requireVerifiedBotSignature | false | true: answer signed requests whose signature verifies under neither Web Bot Auth nor UCP with 403; "agents": also any agent request without a verified signature | | cacheTtlSeconds | 3600 | mandate key cache lifetime (an unknown kid refetches at most once a minute) | | fetch | global fetch | for tests and custom transports | | now | wall clock | seconds since the epoch (tests) | | onEvent | — | agent, mandate, blocked, keys, directory, profile, report, report_skipped, error events; none carries a credential | | keys | — | pinned Enruta public JWKs (offline use) | | platforms | — | extra fingerprints, matched before the built-in table | | reportTimeoutMs | 3000 | when the report has to be awaited |

webBotAuth options: allow / deny (origins, https://*.example.com wildcards; deny wins), algorithms (["ed25519"]; "rsa-pss-sha512" can be added), clockSkewSeconds (60), maxValiditySeconds (86400), requireNonce (false), requireSignedDirectory (false), directoryTtlSeconds (3600), directoryMaxTtlSeconds (86400), failureTtlSeconds (30), directoryTimeoutMs (3000), maxDirectoryBytes (65536), maxDirectoryKeys (64), allowTestKeys (false), nonceStore ({ add(key, expiresAt) }, shared across instances), visaTap (true or { directoryUrl, maxValiditySeconds }), fetch, now.

ucpSignature options: clockSkewSeconds (60), profileTtlSeconds (3600), profileMaxTtlSeconds (86400), failureTtlSeconds (30), profileTimeoutMs (3000), maxProfileBytes (131072), maxProfileKeys (64), maxBodyBytes (the middleware's, 1 MiB), fetch, now.

Accept:

  • inspect(request): Promise<Inspection> — detection and verification, no forwarding; the body is cloned.
  • handle(request, next, ctx?): Promise<Response> — inspect, label, call next(labelledRequest, inspection), report; ctx.waitUntil carries the report when present.
  • verifyMandate(jws, { checkoutId? }): Promise<MandateInspection>.
  • report({ order, mandate_jws, checkout_id?, risk_signals? }, ctx?): Promise<ReportResult | null>.
  • blockResponse(inspection): Response — the AP2 or bot signature error for a blocked inspection.

Inspection:

{
  "request": { "method": "POST", "path": "/checkout-sessions/chk_8f3/complete", "kind": "ucp_complete", "checkout_id": "chk_8f3", "rpc_id": null },
  "agent": {
    "kind": "ucp", "platform": "openai", "profile_url": "https://chatgpt.com/.well-known/agent-profile.json",
    "signed": true, "verified": true, "signature_agent": "https://chatgpt.com", "user_agent": "ChatGPT-User/1.0",
    "web_bot_auth": {
      "signed": true, "verified": true, "status": "verified", "protocol": "web_bot_auth",
      "agent_origin": "https://chatgpt.com", "directory_url": "https://chatgpt.com/.well-known/http-message-signatures-directory",
      "keyid": "poqk…", "tag": "web-bot-auth", "alg": "ed25519", "label": "sig1", "created": 1789048800, "expires": 1789048860,
      "nonce": "…", "components": ["\"@authority\"", "\"@method\"", "\"@path\"", "\"signature-agent\";key=\"sig1\""], "directory_signed": true
    },
    "ucp_signature": {
      "signed": true, "verified": true, "status": "verified", "profile_url": "https://chatgpt.com/.well-known/agent-profile.json",
      "keyid": "poqk…", "alg": "EdDSA", "created": 1789048800, "expires": 1789048860
    }
  },
  "mandate": {
    "present": true, "valid": true, "mandate_id": "mnd_…", "agent": "agt_procurement_02", "principal": "org_halvern",
    "policy": { "id": "pol_…", "version": 3 }, "amount": "168.40", "currency": "USD", "payee": "store.example.com",
    "checkout": { "protocol": "ucp", "reference": "chk_8f3" }, "issued_at": "2026-09-13T10:00:00Z", "expires_at": "2026-09-13T10:15:00Z",
    "mode": "specified", "refill_policy": null, "finality_class": null, "principal_login_id": "halvern/[email protected]",
    "kid": "key_enruta_mandate_2026-09", "reasons": [], "claims": { "…": "…" }
  },
  "mandate_jws": "eyJ…",
  "headers": { "X-Enruta-Agent": "agt_procurement_02", "X-Enruta-Agent-Verified": "true", "X-Enruta-Signature-Agent": "https://chatgpt.com", "X-Enruta-Agent-Profile": "https://chatgpt.com/.well-known/agent-profile.json", "X-Enruta-Mandate-Valid": "true", "…": "…" },
  "risk_signals": [
    { "type": "web_bot_auth_verified", "protocol": "web_bot_auth", "agent_origin": "https://chatgpt.com", "keyid": "poqk…", "tag": "web-bot-auth" },
    { "type": "ucp_signature_verified", "profile_url": "https://chatgpt.com/.well-known/agent-profile.json", "keyid": "poqk…", "alg": "EdDSA" }
  ],
  "block": null
}

mandate.reasons on failure: mandate_missing, malformed, unsupported_alg, unknown_key, keys_unavailable, invalid_signature, not_an_object, expired, payee_mismatch, checkout_mismatch. Claims are surfaced only once the signature verified (an expired mandate is authentic, so its labels are still set with X-Enruta-Mandate-Valid: false; a forged one sets no labels).

Also exported: detectAgent (parses only; pass it the verifications to merge), WebBotAuthVerifier and parseWebBotAuth, UcpSignatureVerifier, parseUcpSignature and ucpAgentProfile for use outside the middleware, signatureBase, parseSignatures, jwkThumbprint, checkContentDigest, PLATFORMS, labelsFor, labelRequest, stripCredentials, extractOrder, decodeCompact, verifyEs256, KeyStore, and the types.

What it does not check

The amount against the checkout total and checkout_hash against the checkout state need the checkout itself; they stay with the merchant's AP2 check (the store keeps doing them).

Web Bot Auth limits:

  • Signature-Agent members of type jwks_uri and cimd are ignored (reason unsupported_discovery_type): only the well-known directory binds keys to a domain.
  • Delegation and certificate chains (x5c, x5u) are not evaluated.
  • Host names that resolve to private addresses cannot be detected with fetch; use allow in front of anything sensitive.
  • Without a shared nonceStore, a nonce is checked per instance only.
  • The Visa TAP body objects (agenticConsumer, agenticPaymentContainer) are not verified.

UCP signature limits:

  • Replay: UCP leaves it to the business layer. The middleware keeps no Idempotency-Key or nonce for UCP signatures; the merchant's idempotency store does that.
  • The profile host is checked by name only: host names that resolve to private addresses cannot be detected with fetch.
  • Keys come from the UCP-Agent header's profile only. An MCP body's meta["ucp-agent"].profile labels the request but resolves no key. Signature-Agent resolution of type jwks_uri or cimd is not used for UCP.
  • ES384 keys and response signatures are not verified.
  • A registry of pre-approved platforms (UCP's profile_not_trusted) is the merchant's policy: ucp_signature.profile_url is the identity to check it against.

Development

npm run typecheck -w @enruta/accept
npm test -w @enruta/accept
npm run build -w @enruta/accept     # dist/accept.mjs, one ESM file, no imports

Apache-2.0.