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

@acme-js/acme

v0.2.0

Published

Minimal, embeddable ACME (RFC 8555) client for Node.js. Zero runtime dependencies, dns-01 only, plugin-based DNS providers.

Readme

@acme-js/acme

Minimal, embeddable ACME (RFC 8555) client for Node.js — a JS alternative to acme.sh. Designed to be embedded by larger host applications to issue, maintain, and renew certificates via dns-01 validation.

  • Zero runtime dependencies. Uses only node:crypto (Web Crypto), native fetch, and node:dns.
  • Node 20+, ESM-only.
  • dns-01 only with a plugin-based DNS provider model.
  • Stateless API. Every call takes keys + URLs explicitly. The library never owns timers, files, or persistent state — the host owns lifecycle.
  • Hand-rolled PKCS#10 CSR encoder (~150 LOC) — the only mandatory non-native piece.
  • Provider-agnostic core. Adding a CA is one ~10-line file; adding a DNS provider is one factory.
  • Hardened by default. HTTPS + same-host URL allowlist, per-request timeouts, AbortSignal cancellation, strict base64url decoding.

Documentation

📖 Full docs in ./docs/ — start there.

| If you want to… | Read | |---|---| | Get started in 5 minutes | Guide → Quickstart | | Look up a function's signature | API reference | | Configure Cloudflare / DigitalOcean / Porkbun / ZeroSSL | Providers | | Write a DNS provider for Route 53 / Gandi / etc. | Custom DNS provider guide | | Debug a failing issuance | Troubleshooting |

Install

pnpm add @acme-js/acme
# or
npm install @acme-js/acme
# or
yarn add @acme-js/acme

Requires Node 20 or newer. ESM-only (import — no require).

Quickstart

Let's Encrypt staging + Cloudflare, ~25 lines:

import {
  AcmeTransport,
  DIRECTORY_LE_STAGING,
  createAccount,
  generateAccountKey,
  generateCertKey,
  createCsr,
  issueCertificate,
} from '@acme-js/acme'
import { cloudflare } from '@acme-js/acme/dns/cloudflare'

// --- one-time account setup (persist the JWK after this!) ---
const accountKeyPair = await generateAccountKey()
const accountJwk = await crypto.subtle.exportKey('jwk', accountKeyPair.publicKey)

const transport = new AcmeTransport({
  directoryUrl: DIRECTORY_LE_STAGING,
  accountKey: accountKeyPair.privateKey,
  accountJwk,
})
const account = await createAccount({
  transport,
  contact: ['mailto:[email protected]'],
  termsOfServiceAgreed: true,
})
// 💾 Persist: accountJwk, accountKeyPair.privateKey (as JWK), account.kid

// --- per-issue ---
const certKeyPair = await generateCertKey()
const csr = await createCsr({
  domains: ['example.com', '*.example.com'],
  privateKey: certKeyPair.privateKey,
  publicKey: certKeyPair.publicKey,
})

const cert = await issueCertificate({
  transport,
  domains: ['example.com', '*.example.com'],
  certKey: certKeyPair.privateKey,
  csr: csr.der,
  dnsProvider: cloudflare({ apiToken: process.env.CF_API_TOKEN! }),
})

console.log(cert.cert)      // PEM leaf
console.log(cert.chain)     // PEM intermediates
console.log(cert.notAfter)  // Date

Swap DIRECTORY_LE_STAGING for DIRECTORY_LE_PROD when ready. Staging issues untrusted certs but has generous rate limits — perfect for testing.

See the guide for workflows: persisting state, renewal, resumable orders, wildcards, cancellation, logging, custom fetch, and more.

Supported providers

ACME / CA (directory URL constants)

| Provider | Constant | EAB | |---|---|---| | Let's Encrypt production | DIRECTORY_LE_PROD | optional | | Let's Encrypt staging | DIRECTORY_LE_STAGING | optional | | ZeroSSL | DIRECTORY_ZEROSSL (from acmejs/acme/zerossl) | required |

ZeroSSL accepts user-supplied EAB credentials OR programmatic fetching:

// Option A: user-supplied (recommended)
import { DIRECTORY_ZEROSSL } from '@acme-js/acme/acme/zerossl'
await createAccount({
  transport,
  eab: { kid: process.env.ZEROSSL_EAB_KID!, hmacKey: process.env.ZEROSSL_EAB_HMAC_KEY! },
})

// Option B: fetched programmatically
import { DIRECTORY_ZEROSSL, getZeroSslEabCredentials } from '@acme-js/acme/acme/zerossl'
const eab = await getZeroSslEabCredentials({ apiKey: process.env.ZEROSSL_ACCESS_KEY! })
await createAccount({ transport, eab })

DNS (challenge providers)

| Provider | Import path | Auth | |---|---|---| | Cloudflare | acmejs/dns/cloudflare | API Token (recommended) or Global API Key | | DigitalOcean | acmejs/dns/digitalocean | Bearer API token | | Porkbun | acmejs/dns/porkbun | API Key + Secret Key (in POST body) |

A minimal in-memory provider (for tests) is ~30 lines; a full real-DNS provider is ~100–200 lines (auth, zone detection, error handling, propagation polling). See Custom DNS provider guide.

Security model

acmejs is designed to be embedded in larger applications, so the threat model matters up front. Highlights:

  • HTTPS + same-host by default. Server-returned URLs (finalize, certificate, challenges[].url, etc.) must be HTTPS and on the same host as the directory — a compromised CA cannot redirect signed POSTs to internal services (SSRF). Pass allowCrossHost: true to opt out (Pebble, multi-host CAs).
  • Per-request timeout (30s default) via AbortController.
  • AbortSignal cancellation on every polling function.
  • Strict base64url decoding for HMAC keys and signatures.
  • Non-extractable cert keys by default (generateCertKeyExtractable() if you need to persist).
  • Domain identifiers validated before flowing into DNS provider URLs.
  • ZeroSSL access key sent in POST body, not URL query string.
  • setTxt is idempotent across all bundled DNS providers. Each one wipes pre-existing TXT records at the challenge hostname before creating the new one, so partial-failure retries and multi-identifier orders (apex + wildcard sharing _acme-challenge.<zone>) don't leave stale records that Boulder reads as (and N more) validation failures.
  • Auth errors surface loudly in all DNS providers (Cloudflare 401/403/429, DigitalOcean 401, Porkbun body-level error codes) instead of silently masquerading as "no zone found".
  • Propagation timings are user-configurable (propagationDelayMs etc.) on every bundled provider — important when the CA's resolver caches stale state longer than the provider's authoritative NS.

Full details: Guide → Security model.

Testing

pnpm test               # unit tests (RFC vectors + mocked fetch)
pnpm test:integration   # Pebble integration (requires docker-compose)

Unit tests use fixed RFC vectors (RFC 7638 Appendix A thumbprint) and an openssl req reference CSR. Provider tests mock fetch — no network. See Troubleshooting → Pebble for the integration setup.

License

MIT