Client and server implementations of ACME (RFC 8555) in TypeScript, for building complete certificate lifecycle management.
| Package | Description |
|---|---|
@peculiar/acme-client |
ACME client |
@peculiar/acme-protocol |
ACME protocol objects |
@peculiar/jose |
JSON Web Signature (JWS) and JSON Web Key (JWK) |
@peculiar/acme-core |
Errors, HTTP content and logging shared by the other packages |
@peculiar/acme-data |
Data model and repository interfaces |
@peculiar/acme-data-memory |
In-memory data layer |
@peculiar/acme-data-dynamodb |
DynamoDB data layer |
@peculiar/acme-server |
ACME server services and controller |
@peculiar/acme-express |
Express middleware for the ACME server |
@peculiar/acme-ra |
Express registration authority with External Account Binding and several CA endpoints |
All packages share one version and are released together.
v2 is ESM-only and requires Node.js ^20.19.0 || >=22.12.0 (1.x had no engines field). Move all @peculiar/acme-* packages and @peculiar/jose to v2 together. CommonJS code can still require() them on these versions; Node.js 22.0–22.11 can't (ERR_REQUIRE_ESM).
- One ESM build per package:
build/cjs,build/es2015andbuild/typesare replaced bybuild/*.jswith.d.tsfiles next to them. Deep imports (@peculiar/acme-core/build/cjs/...) are blocked byexports; import from the package name. - TypeScript compiling to CommonJS: use
"module": "node20"or"nodenext"(node16fails with TS1479). ESM and bundler setups need no changes. You needexperimentalDecoratorsonly for your own@injectable()classes, as before. - Jest in CommonJS mode: switch to ESM mode or Vitest, or configure an ESM-to-CommonJS transformer and make sure
transformIgnorePatternsdoesn't exclude the@peculiar/*packages,asn1jsandtsyringe. - No browser script bundle:
@peculiar/acme-clientno longer shipsbuild/acme.js(theacmeglobal) or thebrowserfield, so<script src="https://unpkg.com/@peculiar/acme-client">stops working. Bundle the package or import it from esm.sh as a module, or pin@peculiar/acme-client@1for the script.
Package-specific changes:
- Peer dependencies: the packages register services into and resolve them from tsyringe's global
container, the same one your app registers into, sotsyringe(^4.10.0) is a peer of-core,-data,-data-memory,-data-dynamodb,-server,-expressand-ra.@peculiar/x509(^3.1.0) is a peer of-data-memory,-data-dynamodb,-serverand-ra(they share itscryptoProviderand pass its objects), and@peculiar/asn1-schema(^3.0.0) of-serverand-ra. npm 7+ installs peers automatically, and a conflicting version now fails at install (ERESOLVE) instead of at runtime. - Reflect polyfill: tsyringe and
@peculiar/x5093 need the Reflect metadata API.@peculiar/acme-core(and so-client) still loadsreflect-metadataitself, but server apps must import a polyfill once, before any other import (see Usage).@peculiar/x5091.x used to load it. - x509 3, asn1-schema 3 and webcrypto 2: certificates and keys from
@peculiar/x5091/2 can't be passed in. The server's defaultcryptoProvideris nowglobalThis.crypto(Node.js WebCrypto) instead of a new@peculiar/webcryptoinstance; passcryptoProviderexplicitly if you create keys with another WebCrypto implementation (use@peculiar/webcrypto2: next to x509 3, 1.x fails ECDSA signing withCannot get schema for 'EcDsaSignature' target). - Strict base64url: following RFC 7515 and RFC 8555 §6.1, the server and
@peculiar/josereject base64url values with=padding (JWS segments, thefinalizeCSR, therevokeCertcertificate), andfinalizeno longer accepts a CSR in PEM, hex or standard Base64; it answersbadCSR. - DynamoDB:
@peculiar/acme-data-dynamodbmoves from dynamoose 2 and aws-sdk v2 (end-of-life) to dynamoose 4 and AWS SDK v3.DependencyInjection.registerAsync()takes an AWS SDK v3DynamoDBClientConfigasclient, so credentials go incredentials: { accessKeyId, secretAccessKey }. The table layout and stored items are unchanged. - Dependencies:
node-fetch(the client uses the globalfetch),pvtsutils(replaced by@peculiar/utils4) andtslibare gone.
An ACME server with the in-memory data layer:
// tsyringe and @peculiar/x509 need the Reflect metadata API: import a polyfill first.
import "reflect-metadata";
import express from "express";
import { container } from "tsyringe";
import { DependencyInjection as dataMemory } from "@peculiar/acme-data-memory";
import { AcmeExpress } from "@peculiar/acme-express";
import { diEndpointService } from "@peculiar/acme-server";
const app = express();
AcmeExpress.register(app, { baseAddress: "http://localhost:4000/acme" });
dataMemory.register(container);
// Register the service that issues certificates (see packages/test-server for an example).
container.register(diEndpointService, MyEndpointService);
app.listen(4000);A client (Node.js has fetch and WebCrypto built in):
import { ApiClient } from "@peculiar/acme-client";
const keys = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]);
const client = await ApiClient.create(keys, "http://localhost:4000/acme/directory");
await client.newAccount({ contact: ["mailto:admin@example.com"], termsOfServiceAgreed: true });See the package READMEs for more.
See CONTRIBUTING.md. In short:
npm ci
npm run build
npm test