{"_id":"@acme-js/acme","_rev":"2-ef77d74f4b148224541b346203252920","name":"@acme-js/acme","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@acme-js/acme","version":"0.1.0","keywords":["acme","rfc8555","letsencrypt","zerossl","certificate","dns-01","tls","ssl"],"license":"MIT","_id":"@acme-js/acme@0.1.0","maintainers":[{"name":"reazuliqbal","email":"hi@reaz.dev"}],"homepage":"https://github.com/acmejs/acme","bugs":{"url":"https://github.com/acmejs/acme/issues"},"dist":{"shasum":"9566a84f94c340520902822c45efc8cdd976f19e","tarball":"https://registry.npmjs.org/@acme-js/acme/-/acme-0.1.0.tgz","fileCount":103,"integrity":"sha512-5fFWyVQ+kFESe5bSCxBlYV4sl3raqi4ppiSjR4HqIeefKLSyef6ElY4W0bq+BOtF5092Qu3aNr8+3AKeTj7CxA==","signatures":[{"sig":"MEYCIQC5SkoU/smdLlch/o9IL3l0HmaX7POF3OjrM75c8ZcWsQIhAMWlqsAwE7U0tAziGNRs6NBkriGKcVpGClaoOy83RxPH","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":221230},"type":"module","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./csr":{"types":"./dist/csr/index.d.ts","import":"./dist/csr/index.js"},"./crypto":{"types":"./dist/crypto/index.d.ts","import":"./dist/crypto/index.js"},"./dns/porkbun":{"types":"./dist/dns/porkbun.d.ts","import":"./dist/dns/porkbun.js"},"./acme/zerossl":{"types":"./dist/acme/zerossl.d.ts","import":"./dist/acme/zerossl.js"},"./dns/cloudflare":{"types":"./dist/dns/cloudflare.d.ts","import":"./dist/dns/cloudflare.js"},"./acme/letsencrypt":{"types":"./dist/acme/letsencrypt.d.ts","import":"./dist/acme/letsencrypt.js"},"./dns/digitalocean":{"types":"./dist/dns/digitalocean.d.ts","import":"./dist/dns/digitalocean.js"}},"gitHead":"873fb8fa848c5bddb83538bbfd18b47580a5463e","scripts":{"lint":"oxlint","test":"vitest run","build":"tsc","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm build && pnpm lint && pnpm typecheck && pnpm test && publint","test:integration":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"reazuliqbal","email":"hi@reaz.dev"},"repository":{"url":"git+https://github.com/acmejs/acme.git","type":"git"},"_npmVersion":"11.16.0","description":"Minimal, embeddable ACME (RFC 8555) client for Node.js. Zero runtime dependencies, dns-01 only, plugin-based DNS providers.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","_hasShrinkwrap":false,"packageManager":"pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c","devDependencies":{"oxlint":"^1.76.0","vitest":"^2.1.9","publint":"^0.3.22","typescript":"^5.9.3","@types/node":"^22.20.1"},"_npmOperationalInternal":{"tmp":"tmp/acme_0.1.0_1785401297817_0.9498857539494505","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@acme-js/acme","version":"0.2.0","description":"Minimal, embeddable ACME (RFC 8555) client for Node.js. Zero runtime dependencies, dns-01 only, plugin-based DNS providers.","type":"module","engines":{"node":">=20"},"license":"MIT","sideEffects":false,"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./crypto":{"types":"./dist/crypto/index.d.ts","import":"./dist/crypto/index.js"},"./csr":{"types":"./dist/csr/index.d.ts","import":"./dist/csr/index.js"},"./acme/letsencrypt":{"types":"./dist/acme/letsencrypt.d.ts","import":"./dist/acme/letsencrypt.js"},"./acme/zerossl":{"types":"./dist/acme/zerossl.d.ts","import":"./dist/acme/zerossl.js"},"./dns/cloudflare":{"types":"./dist/dns/cloudflare.d.ts","import":"./dist/dns/cloudflare.js"},"./dns/digitalocean":{"types":"./dist/dns/digitalocean.d.ts","import":"./dist/dns/digitalocean.js"},"./dns/porkbun":{"types":"./dist/dns/porkbun.d.ts","import":"./dist/dns/porkbun.js"}},"scripts":{"build":"tsc","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:integration":"vitest run --config vitest.integration.config.ts","lint":"oxlint","prepublishOnly":"pnpm build && pnpm lint && pnpm typecheck && pnpm test && publint"},"keywords":["acme","rfc8555","letsencrypt","zerossl","certificate","dns-01","tls","ssl"],"repository":{"type":"git","url":"git+https://github.com/acmejs/acme.git"},"homepage":"https://github.com/acmejs/acme","bugs":{"url":"https://github.com/acmejs/acme/issues"},"devDependencies":{"@types/node":"^22.20.1","oxlint":"^1.76.0","publint":"^0.3.22","typescript":"^5.9.3","vitest":"^2.1.9"},"packageManager":"pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c","gitHead":"9be5abe42f3fde2d99ffc5a027f3074f99cb33d1","_id":"@acme-js/acme@0.2.0","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-2ucFObKQLPPBm40RjOLtLx7hFPH78rPRp8rsbVEth+PkxitGNiEMuqQXccvBPaFP+hoDaoVBSFAf2lAc5oGSSA==","shasum":"e635d1497a86a53afb97e8e33f63035e6436efd0","tarball":"https://registry.npmjs.org/@acme-js/acme/-/acme-0.2.0.tgz","fileCount":103,"unpackedSize":235127,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDYL2GLOSYT8NB36IMh3dgM1SKhWkoxkPu4b6rBXpKjsAIgCHii43fV/10lCT2I3HM9oNfaVbhtMpoa4jCpz1/Ec+I="}]},"_npmUser":{"name":"reazuliqbal","email":"hi@reaz.dev"},"directories":{},"maintainers":[{"name":"reazuliqbal","email":"hi@reaz.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/acme_0.2.0_1785651375554_0.3252637578864448"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-30T08:48:17.685Z","modified":"2026-08-02T06:16:15.864Z","0.1.0":"2026-07-30T08:48:17.965Z","0.2.0":"2026-08-02T06:16:15.702Z"},"bugs":{"url":"https://github.com/acmejs/acme/issues"},"license":"MIT","homepage":"https://github.com/acmejs/acme","keywords":["acme","rfc8555","letsencrypt","zerossl","certificate","dns-01","tls","ssl"],"repository":{"type":"git","url":"git+https://github.com/acmejs/acme.git"},"description":"Minimal, embeddable ACME (RFC 8555) client for Node.js. Zero runtime dependencies, dns-01 only, plugin-based DNS providers.","maintainers":[{"name":"reazuliqbal","email":"hi@reaz.dev"}],"readme":"# @acme-js/acme\n\nMinimal, embeddable ACME (RFC 8555) client for Node.js — a JS alternative to\n`acme.sh`. Designed to be embedded by larger host applications to issue,\nmaintain, and renew certificates via **dns-01** validation.\n\n- **Zero runtime dependencies.** Uses only `node:crypto` (Web Crypto),\n  native `fetch`, and `node:dns`.\n- **Node 20+**, ESM-only.\n- **dns-01 only** with a plugin-based DNS provider model.\n- **Stateless API.** Every call takes keys + URLs explicitly. The library\n  never owns timers, files, or persistent state — the host owns lifecycle.\n- **Hand-rolled PKCS#10 CSR encoder** (~150 LOC) — the only mandatory\n  non-native piece.\n- **Provider-agnostic core.** Adding a CA is one ~10-line file; adding a\n  DNS provider is one factory.\n- **Hardened by default.** HTTPS + same-host URL allowlist, per-request\n  timeouts, `AbortSignal` cancellation, strict base64url decoding.\n\n## Documentation\n\n📖 **[Full docs in `./docs/`](./docs/README.md)** — start there.\n\n| If you want to… | Read |\n|---|---|\n| Get started in 5 minutes | [Guide → Quickstart](./docs/guide.md#quickstart) |\n| Look up a function's signature | [API reference](./docs/api.md) |\n| Configure Cloudflare / DigitalOcean / Porkbun / ZeroSSL | [Providers](./docs/providers.md) |\n| Write a DNS provider for Route 53 / Gandi / etc. | [Custom DNS provider guide](./docs/custom-dns-provider.md) |\n| Debug a failing issuance | [Troubleshooting](./docs/troubleshooting.md) |\n\n## Install\n\n```sh\npnpm add @acme-js/acme\n# or\nnpm install @acme-js/acme\n# or\nyarn add @acme-js/acme\n```\n\nRequires Node 20 or newer. ESM-only (`import` — no `require`).\n\n## Quickstart\n\nLet's Encrypt staging + Cloudflare, ~25 lines:\n\n```ts\nimport {\n  AcmeTransport,\n  DIRECTORY_LE_STAGING,\n  createAccount,\n  generateAccountKey,\n  generateCertKey,\n  createCsr,\n  issueCertificate,\n} from '@acme-js/acme'\nimport { cloudflare } from '@acme-js/acme/dns/cloudflare'\n\n// --- one-time account setup (persist the JWK after this!) ---\nconst accountKeyPair = await generateAccountKey()\nconst accountJwk = await crypto.subtle.exportKey('jwk', accountKeyPair.publicKey)\n\nconst transport = new AcmeTransport({\n  directoryUrl: DIRECTORY_LE_STAGING,\n  accountKey: accountKeyPair.privateKey,\n  accountJwk,\n})\nconst account = await createAccount({\n  transport,\n  contact: ['mailto:admin@example.com'],\n  termsOfServiceAgreed: true,\n})\n// 💾 Persist: accountJwk, accountKeyPair.privateKey (as JWK), account.kid\n\n// --- per-issue ---\nconst certKeyPair = await generateCertKey()\nconst csr = await createCsr({\n  domains: ['example.com', '*.example.com'],\n  privateKey: certKeyPair.privateKey,\n  publicKey: certKeyPair.publicKey,\n})\n\nconst cert = await issueCertificate({\n  transport,\n  domains: ['example.com', '*.example.com'],\n  certKey: certKeyPair.privateKey,\n  csr: csr.der,\n  dnsProvider: cloudflare({ apiToken: process.env.CF_API_TOKEN! }),\n})\n\nconsole.log(cert.cert)      // PEM leaf\nconsole.log(cert.chain)     // PEM intermediates\nconsole.log(cert.notAfter)  // Date\n```\n\nSwap `DIRECTORY_LE_STAGING` for `DIRECTORY_LE_PROD` when ready. Staging\nissues untrusted certs but has generous rate limits — perfect for testing.\n\nSee the [**guide**](./docs/guide.md) for workflows: persisting state,\nrenewal, resumable orders, wildcards, cancellation, logging, custom\nfetch, and more.\n\n## Supported providers\n\n### ACME / CA (directory URL constants)\n\n| Provider | Constant | EAB |\n|---|---|---|\n| Let's Encrypt production | `DIRECTORY_LE_PROD` | optional |\n| Let's Encrypt staging | `DIRECTORY_LE_STAGING` | optional |\n| ZeroSSL | `DIRECTORY_ZEROSSL` (from `acmejs/acme/zerossl`) | **required** |\n\nZeroSSL accepts user-supplied EAB credentials OR programmatic fetching:\n\n```ts\n// Option A: user-supplied (recommended)\nimport { DIRECTORY_ZEROSSL } from '@acme-js/acme/acme/zerossl'\nawait createAccount({\n  transport,\n  eab: { kid: process.env.ZEROSSL_EAB_KID!, hmacKey: process.env.ZEROSSL_EAB_HMAC_KEY! },\n})\n\n// Option B: fetched programmatically\nimport { DIRECTORY_ZEROSSL, getZeroSslEabCredentials } from '@acme-js/acme/acme/zerossl'\nconst eab = await getZeroSslEabCredentials({ apiKey: process.env.ZEROSSL_ACCESS_KEY! })\nawait createAccount({ transport, eab })\n```\n\n### DNS (challenge providers)\n\n| Provider | Import path | Auth |\n|---|---|---|\n| Cloudflare | `acmejs/dns/cloudflare` | API Token (recommended) or Global API Key |\n| DigitalOcean | `acmejs/dns/digitalocean` | Bearer API token |\n| Porkbun | `acmejs/dns/porkbun` | API Key + Secret Key (in POST body) |\n\nA minimal in-memory provider (for tests) is ~30 lines; a full real-DNS\nprovider is ~100–200 lines (auth, zone detection, error handling, propagation\npolling). See\n[**Custom DNS provider guide**](./docs/custom-dns-provider.md).\n\n## Security model\n\nacmejs is designed to be embedded in larger applications, so the threat\nmodel matters up front. Highlights:\n\n- **HTTPS + same-host by default.** Server-returned URLs (`finalize`,\n  `certificate`, `challenges[].url`, etc.) must be HTTPS and on the same\n  host as the directory — a compromised CA cannot redirect signed POSTs to\n  internal services (SSRF). Pass `allowCrossHost: true` to opt out (Pebble,\n  multi-host CAs).\n- **Per-request timeout (30s default)** via `AbortController`.\n- **`AbortSignal` cancellation** on every polling function.\n- **Strict base64url decoding** for HMAC keys and signatures.\n- **Non-extractable cert keys** by default (`generateCertKeyExtractable()`\n  if you need to persist).\n- **Domain identifiers validated** before flowing into DNS provider URLs.\n- **ZeroSSL access key** sent in POST body, not URL query string.\n- **`setTxt` is idempotent across all bundled DNS providers.** Each one\n  wipes pre-existing TXT records at the challenge hostname before creating\n  the new one, so partial-failure retries and multi-identifier orders\n  (apex + wildcard sharing `_acme-challenge.<zone>`) don't leave stale\n  records that Boulder reads as `(and N more)` validation failures.\n- **Auth errors surface loudly** in all DNS providers (Cloudflare 401/403/429,\n  DigitalOcean 401, Porkbun body-level error codes) instead of silently\n  masquerading as \"no zone found\".\n- **Propagation timings are user-configurable** (`propagationDelayMs` etc.)\n  on every bundled provider — important when the CA's resolver caches stale\n  state longer than the provider's authoritative NS.\n\nFull details: [**Guide → Security model**](./docs/guide.md#security-model).\n\n## Testing\n\n```sh\npnpm test               # unit tests (RFC vectors + mocked fetch)\npnpm test:integration   # Pebble integration (requires docker-compose)\n```\n\nUnit tests use fixed RFC vectors (RFC 7638 Appendix A thumbprint) and an\n`openssl req` reference CSR. Provider tests mock `fetch` — no network. See\n[**Troubleshooting → Pebble**](./docs/troubleshooting.md#local-integration-testing-with-pebble)\nfor the integration setup.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}