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

@push.rocks/smartacme

v11.0.0

Published

A TypeScript-based ACME client and server for certificate management with built-in CA, supporting LetsEncrypt and custom ACME authorities.

Readme

@push.rocks/smartacme

A TypeScript-based ACME client and server for certificate management with a focus on simplicity and power. Includes a full RFC 8555-compliant ACME client for Let's Encrypt and a built-in ACME Directory Server for running your own Certificate Authority.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm add @push.rocks/smartacme

smartacme does not ship a DNS provider. For DNS-01 through Cloudflare, install the Cloudflare client next to it:

pnpm add @apiclient.xyz/cloudflare

Ensure your project uses TypeScript and ECMAScript Modules (ESM).

Usage

@push.rocks/smartacme automates the full ACME certificate lifecycle — obtaining, renewing, and storing SSL/TLS certificates from Let's Encrypt. It features a built-in RFC 8555-compliant ACME protocol implementation, pluggable challenge handlers (DNS-01, HTTP-01), pluggable certificate storage backends (MongoDB, in-memory, or your own), structured error handling with smart retry logic, and built-in concurrency control with rate limiting to keep you safely within Let's Encrypt limits.

🚀 Quick Start

import { SmartAcme, certmanagers, handlers } from '@push.rocks/smartacme';
import * as cloudflare from '@apiclient.xyz/cloudflare';

// 1. Set up a certificate manager (MongoDB or in-memory)
const certManager = new certmanagers.MongoCertManager({
  mongoDbUrl: 'mongodb://localhost:27017',
  mongoDbName: 'myapp',
  mongoDbPass: 'secret',
});

// 2. Set up challenge handlers
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_API_TOKEN');
const dnsHandler = new handlers.Dns01Handler(cfAccount.getConvenientDnsProvider());

// 3. Create and start SmartAcme
const smartAcme = new SmartAcme({
  accountEmail: '[email protected]',
  certManager,
  environment: 'production', // or 'integration' for staging
  challengeHandlers: [dnsHandler],
});

await smartAcme.start();

// 4. Get a certificate
const cert = await smartAcme.getCertificateForDomain('example.com');
console.log(cert.publicKey);  // PEM certificate chain
// Supply cert.privateKey directly to your TLS consumer; never log it.

// 5. Clean up
await smartAcme.stop();

⚙️ SmartAcme Options

interface ISmartAcmeOptions {
  accountEmail: string;                      // ACME account email
  accountPrivateKey?: string;                // Optional account key (auto-generated if omitted)
  certManager: ICertManager;                 // Certificate storage backend
  environment: 'production' | 'integration'; // Let's Encrypt environment
  challengeHandlers: IChallengeHandler[];    // At least one handler required
  challengePriority?: string[];              // e.g. ['dns-01', 'http-01']
  retryOptions?: {                           // Optional retry/backoff config
    retries?: number;                        // Default: 10
    factor?: number;                         // Default: 4
    minTimeoutMs?: number;                   // Default: 1000
    maxTimeoutMs?: number;                   // Default: 60000
  };
  // Concurrency & rate limiting
  maxConcurrentIssuances?: number;           // Global cap on parallel ACME ops (default: 5)
  maxOrdersPerWindow?: number;               // Max orders in sliding window (default: 250)
  orderWindowMs?: number;                    // Sliding window duration in ms (default: 3 hours)
  directoryUrl?: string;                     // Explicit HTTPS CA directory; loopback HTTP for tests
  exactIssuanceStore?: IExactIssuanceStore;   // Durable exact-name account/order/certificate adapter
  maxPendingExactRequests?: number;          // Bounded in-flight exact identities (default: 64)
  exactDnsPropagationDelayMs?: number;       // DNS propagation grace period (default: 60000)
  renewThresholdDays?: number;               // Renewal margin in days, capped at 1/3 of the lifetime (default: 30)
  installSignalHandlers?: boolean;           // Own SIGINT/SIGTERM and exit the process on them (default: true)
}

🛑 Process Signals and Shutdown

By default, start() installs SIGINT and SIGTERM handlers. On either signal they call stop() and then end the process with process.exit(). stop() removes these handlers again, and a later start() installs them once more, so a start/stop cycle never leaves listeners behind.

An instance moves through stopped → starting → started → stopping → stopped. start() runs only from stopped: it refuses a second call while the first is still running (SmartAcme is already starting), after it succeeded (SmartAcme is already started) and during a stop (SmartAcme is stopping). stop() works from every state, and a concurrent stop() joins the one in progress. A stop() during starting cancels the start: the start rejects with SmartAcmeStartCancelledError, and stop() waits for it to settle before it releases what the start already created, so it resolves with nothing left running and the instance stopped. A new start() then works as on a fresh instance.

A start() that fails for any other reason, for example because the CA refuses the account registration, runs the same ordered release as stop() before it rejects, so it too ends stopped with nothing running: the certificate store is closed, the DNS client is terminated and no signal listener is left. It rejects with its own error. If the release fails as well, it rejects with an AggregateError whose errors hold the start error first and the release failure second.

stop() leaves no challenge behind. It cancels running issuances: ACME requests, retry backoff, the DNS-01 propagation poll and the DNS-01 cool-down all end on stop, and each issuance removes its prepared challenges before stop() goes on. A challenge whose cleanup failed, during issuance or on stop, stays pending; stop() tries it once more, rejects with the failure if it still fails, and a later stop() tries again. Only then does stop() terminate the DNS client and close the certificate store. Every wait SmartAcme owns is cancelled, so what bounds stop() is your own code: challenge handler prepare()/cleanup() calls and certificate manager calls must settle.

A host process that has its own shutdown sequence, such as a daemon that stops several services in order, must not let a library end the process under it. Pass installSignalHandlers: false, and SmartAcme adds no process signal listeners; the host calls stop() as part of its own shutdown:

const smartAcme = new SmartAcme({
  accountEmail: '[email protected]',
  certManager,
  environment: 'production',
  challengeHandlers: [dnsHandler],
  installSignalHandlers: false,
});
await smartAcme.start();

process.once('SIGTERM', async () => {
  // ... stop the host's other services ...
  await smartAcme.stop();
});

📜 Getting Certificates

// Standard certificate for a single domain
const cert = await smartAcme.getCertificateForDomain('example.com');

// Include wildcard coverage (requires DNS-01 handler)
// Issues a single cert covering example.com AND *.example.com
const certWithWildcard = await smartAcme.getCertificateForDomain('example.com', {
  includeWildcard: true,
});

// Request wildcard only
const wildcardCert = await smartAcme.getCertificateForDomain('*.example.com');

Certificates are cached and reused until they are due for renewal: once at most renewThresholdDays (default 30) or one third of the certificate's lifetime remains, whichever is smaller. For a 90-day certificate both margins are 30 days; the cap keeps short-lived certificates from being reissued on every request. The next getCertificateForDomain() call renews a due certificate, and forceRenew: true renews regardless. Lifetime and expiry are read from the issued X.509 certificate. A due certificate stays in the store until its replacement overwrites it, so a failed renewal leaves the still-valid certificate in place.

Exact identifiers and durable order recovery

Use getCertificateForIdentifiers() for independently authorized hostnames. It supports nested names such as alice.dev.example.com, canonicalizes IDNA, case, and trailing dots, and never adds a parent or wildcard identifier. Explicit wildcards remain supported when deliberately included by the caller. The embedding application must authorize the entire requested identifier set.

Configure exactIssuanceStore before start(). Its public IExactIssuanceStore interface owns durable account keys, revision-checked issuance records, and completed certificates. createAccountKey() must insert only if absent; compareAndSetIssuance() must atomically insert on a null revision or replace only the expected revision. Acknowledgement means the write is durable. No filesystem or implicit in-memory persistence fallback is provided.

// smartAcme has been started with your durable exactIssuanceStore adapter.
const request = {
  namespace: 'testing-grant:stable-grant-id',
  identifiers: ['alice.dev.example.com'],
};
const result = await smartAcme.getCertificateForIdentifiers(request);
if (result.status === 'ready') {
  // Pass result.certificate.publicKey and .privateKey to the authorized TLS consumer.
  // Metadata includes validFrom, validUntil, and renewAfter in milliseconds.
} else {
  // Persist a job reference and poll according to your worker policy.
  console.log(result.issuance.status); // Safe projection; contains no secret material.
}

Certificate identity binds the namespace, sorted exact identifiers, directory URL, and account thumbprint. Different namespaces never reuse private keys. Cached material is checked for the exact SAN set, matching private key, and validity before delivery. Valid cache reads consume no issuance slot. Renewal is on demand at renewAfter, with a new key and order; there is no background renewal timer. The renewal margin is the smaller of renewThresholdDays (default 30) or one third of the certificate lifetime. It is fixed into renewAfter at issuance, so a certificate issued under an earlier threshold keeps its stored renewAfter until it is renewed.

The exact path requires DNS-01 and verifies handler support before creating an order. Handler prepare/cleanup must be idempotent and cleanup must remove only the supplied TXT value. Cleanup intent is retained until cleanup succeeds. Exact work has two concurrent slots within the configured global capacity, bounded admission, and an in-flight map that releases entries on completion. Secret-bearing results do not enter Taskbuffer's result-sharing cache. For exact issuance, stop() closes admission, rejects queued callers, aborts active protocol requests and propagation waits, and drains their cleanup before closing the transport and storage. stop() resolves once the DNS client's Rust process has exited; if that fails, it rejects after still closing the certificate store, and a later stop() tries the DNS client again. Custom challenge callbacks must settle for shutdown to finish; their external effects remain the callback owner's responsibility.

getCertificateIdentity(request) binds a durable job to the current namespace, identifiers, issuer, and account without placing an order. getCertificateIssuanceStatus(request) returns only safe status metadata.

An attempt that can no longer complete is replaced by the next getCertificateForIdentifiers() call, with a new key and order, once its issuance.retryAfter has passed. Such an attempt is one that failed (the CA refused the order for a rate limit, invalidated the order or an authorization, or issued a certificate that fails validation), one that was abandoned (abandoned_unverified), and one whose order-creation response was lost (creating, or indeterminate without an order). retryAfter is the CA's retry time after a rate-limit refusal, at least one minute, and otherwise one hour after the attempt's last update. A pending result without retryAfter belongs to an attempt that can still complete.

An order whose creation response was lost cannot be finalized: its URL was never recorded, and a late response can no longer be recorded once the replacement took the issuance record's revision. The hour far outlasts an order-creation request, so a creation still in flight in another process is never superseded, and it spaces a failing name's orders out to Let's Encrypt's hourly failed-validation limit. Concurrent callers race on the revision compare-and-set: exactly one places the replacement order, and the others receive its attempt as pending. The lost order is not looked up, because RFC 8555's per-account order list is optional and Let's Encrypt does not provide it. Let's Encrypt currently answers a new order request with a still pending or ready order of the same account and identifiers, so there the replacement continues the lost order.

An attempt holding an acknowledged order keeps its original key, CSR, and order handle through restarts, and it is never replaced while that order can still issue unless it was abandoned. A lost finalization response produces indeterminate rather than a new CSR, and every request reads the order again: the attempt completes when the CA issued the certificate and becomes failed with order_invalid when the CA invalidated the order, at the latest when the order expires.

recoverCertificateIssuance({ ...request, issuanceId, expectedRevision, action }) accepts recheck, retry-proven-unapplied, or abandon; the embedding service must authorize administrative recovery. recheck reads an acknowledged order and can recover an issued certificate. retry-proven-unapplied retries a rate-limited attempt under its own key once the CA's retry time passed. abandon ends an attempt that has not completed: its challenges are cleaned up and the next request replaces it an hour later. It claims no CA rollback, and a certificate the CA still issues for an abandoned order is never delivered.

Store adapters keep each issuance record until its certificate key is retired and enforce admission quotas at their application boundary; they never have to delete a record to unblock issuance. Issuance records contain private keys, CSRs, and challenge values: do not serialize them into status APIs or logs. Credential/grant revocation belongs to the embedding application; it cannot erase material previously delivered to a client.

Custom challenge handlers receive an optional second IChallengeContext argument on prepare, verify, and cleanup. SmartAcme always supplies its opaque operationId: it stays the same for an order's retries, cleanup, and exact issuance recovery after restart. Renewal and isolated issuances get separate owners. Combine it with the challenge type and input to key durable resource intent; do not infer ownership from a hostname or TXT value alone. Existing handlers accepting one argument remain supported. Built-in DNS adapters retain the ownership semantics of their configured provider.

The normal pnpm test suite uses isolated protocol fixtures. The live Cloudflare test is skipped unless SMARTACME_LIVE_DNS_TEST=1 is explicitly set. With separate authorization for public DNS changes and staging-CA issuance, run SMARTACME_LIVE_DNS_TEST=1 pnpm exec tstest test/test.smartacme.integration.ts --verbose --logfile --timeout 600. That test uses the CF_TOKEN credential and issues for bleu.de and *.bleu.de.

The suite also runs the ACME server test under Deno, which reads the committed deno.lock. After changing dependencies, regenerate that lock from the public registry and confirm it resolves on its own:

NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ deno install --lockfile-only --lock=deno.lock --frozen=false
NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ deno install --lockfile-only --frozen

The first command leaves node_modules alone. It pins every package by its SHA-512 digest and writes no tarball URL, because the packages come from the default npm registry. Without the NPM_CONFIG_REGISTRY override, a configured registry mirror would write its tarball URLs into every entry, and Deno 2.9.7 refuses a lock whose URLs name a registry other than the one it is using.

📦 Certificate Object

The returned SmartacmeCert (also exported as Cert) object has these properties:

| Property | Type | Description | |-------------|----------|--------------------------------------| | id | string | Unique certificate identifier | | domainName| string | Domain the cert is issued for | | publicKey | string | PEM-encoded certificate chain | | privateKey| string | PEM-encoded private key | | csr | string | Certificate Signing Request | | created | number | Timestamp of creation | | validUntil| number | Timestamp of expiration |

Useful methods:

cert.isStillValid();      // true if not expired
cert.shouldBeRenewed();   // true once no more than min(30 days, lifetime / 3) remains
cert.shouldBeRenewed(10); // same rule with a 10-day threshold

🔀 Concurrency Control & Rate Limiting

When many callers request certificates concurrently (e.g., hundreds of subdomains under the same TLD), SmartAcme automatically handles deduplication, concurrency, and rate limiting using a built-in task manager powered by @push.rocks/taskbuffer.

How It Works

Three constraint layers protect your ACME account:

| Layer | What It Does | Default | |-------|-------------|---------| | Per-domain mutex | Only one issuance runs per base domain at a time. Concurrent requests for the same domain automatically wait and receive the same certificate result. | 1 concurrent per domain | | Global concurrency cap | Limits total parallel ACME operations across all domains. | 5 concurrent | | Account rate limit | Sliding-window rate limiter that keeps you under Let's Encrypt's 300 orders/3h account limit. | 250 per 3 hours |

🛡️ Automatic Request Deduplication

If 100 requests come in for subdomains of example.com simultaneously, only one ACME issuance runs. All other callers automatically wait and receive the same certificate — no duplicate orders, no wasted rate limit budget.

// These all resolve to the same certificate with a single ACME order:
const results = await Promise.all([
  smartAcme.getCertificateForDomain('app.example.com'),
  smartAcme.getCertificateForDomain('api.example.com'),
  smartAcme.getCertificateForDomain('cdn.example.com'),
]);

⚡ Configuring Limits

const smartAcme = new SmartAcme({
  accountEmail: '[email protected]',
  certManager,
  environment: 'production',
  challengeHandlers: [dnsHandler],
  maxConcurrentIssuances: 10,     // Allow up to 10 parallel ACME issuances
  maxOrdersPerWindow: 200,        // Cap at 200 orders per window
  orderWindowMs: 2 * 60 * 60_000, // 2-hour sliding window
});

📊 Observing Issuance Progress

Subscribe to the certIssuanceEvents stream to observe certificate issuance progress in real-time:

smartAcme.certIssuanceEvents.subscribe((event) => {
  switch (event.type) {
    case 'started':
      console.log(`🔄 Issuance started: ${event.task.name}`);
      break;
    case 'step':
      console.log(`📍 Step: ${event.stepName} (${event.task.currentProgress}%)`);
      break;
    case 'completed':
      console.log(`✅ Issuance completed: ${event.task.name}`);
      break;
    case 'failed':
      console.log(`❌ Issuance failed: ${event.error}`);
      break;
  }
});

Each issuance goes through four steps: prepare (10%) → authorize (40%) → finalize (30%) → store (20%).

Certificate Managers

SmartAcme uses the ICertManager interface for pluggable certificate storage.

🗄️ MongoCertManager

Persistent storage backed by MongoDB using @lossless.org/client/nosqldb:

import { certmanagers } from '@push.rocks/smartacme';

const certManager = new certmanagers.MongoCertManager({
  mongoDbUrl: 'mongodb://localhost:27017',
  mongoDbName: 'myapp',
  mongoDbPass: 'secret',
});

🧪 MemoryCertManager

In-memory storage, ideal for testing or ephemeral workloads:

import { certmanagers } from '@push.rocks/smartacme';

const certManager = new certmanagers.MemoryCertManager();

🔧 Custom Certificate Manager

Implement the ICertManager interface for your own storage backend:

import type { ICertManager, Cert } from '@push.rocks/smartacme';

class RedisCertManager implements ICertManager {
  async init(): Promise<void> { /* connect */ }
  async retrieveCertificate(domainName: string): Promise<Cert | null> { /* lookup */ }
  async storeCertificate(cert: Cert): Promise<void> { /* save */ }
  async deleteCertificate(domainName: string): Promise<void> { /* remove */ }
  async close(): Promise<void> { /* disconnect */ }
  async wipe(): Promise<void> { /* clear all */ }
}

Challenge Handlers

SmartAcme ships with three built-in ACME challenge handlers. All implement IChallengeHandler<T>.

🌐 Dns01Handler

Sets and removes DNS TXT records for dns-01 challenges through any IConvenientDnsProvider from @tsclass/tsclass. For Cloudflare, install @apiclient.xyz/cloudflare yourself and pass its provider:

import { handlers } from '@push.rocks/smartacme';
import * as cloudflare from '@apiclient.xyz/cloudflare';

const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
const dnsHandler = new handlers.Dns01Handler(cfAccount.getConvenientDnsProvider());

The optional second argument is a Smartdns client from @push.rocks/smartdns/client ^9.0.0, the major this package depends on; without it the handler creates its own.

Pass the provider from getConvenientDnsProvider(), not the CloudflareAccount itself. The provider rejects when Cloudflare fails, so a challenge record whose removal failed stays recorded for another cleanup attempt. A record that is already gone counts as removed. The account's deprecated convenience methods log provider failures and resolve anyway.

DNS-01 is required for wildcard certificates and works regardless of server accessibility.

📁 Http01Webroot

Writes challenge response files to a filesystem webroot for http-01 validation:

import { handlers } from '@push.rocks/smartacme';

const httpHandler = new handlers.Http01Webroot({
  webroot: '/var/www/html',
});

The handler writes to <webroot>/.well-known/acme-challenge/<token> and cleans up after validation.

🧠 Http01MemoryHandler

In-memory HTTP-01 handler — stores challenge tokens in memory and serves them via handleRequest():

import { handlers } from '@push.rocks/smartacme';

const memHandler = new handlers.Http01MemoryHandler();

// Integrate with any HTTP server (Express, Koa, raw http, etc.)
app.use((req, res, next) => memHandler.handleRequest(req, res, next));

Perfect for serverless or container environments where filesystem access is limited.

🔧 Custom Challenge Handler

Implement IChallengeHandler<T> for custom challenge types:

import type { handlers } from '@push.rocks/smartacme';

interface MyChallenge {
  type: string;
  token: string;
  keyAuthorization: string;
}

class MyHandler implements handlers.IChallengeHandler<MyChallenge> {
  getSupportedTypes(): string[] { return ['http-01']; }
  async prepare(ch: MyChallenge): Promise<void> { /* set up challenge response */ }
  async cleanup(ch: MyChallenge): Promise<void> { /* tear down */ }
  async checkWetherDomainIsSupported(domain: string): Promise<boolean> { return true; }
}

Error Handling

SmartAcme provides structured ACME error handling via the AcmeError class, which carries full RFC 8555 error information:

import { AcmeError } from '@push.rocks/smartacme/ts/acme/acme.classes.error.js';

try {
  const cert = await smartAcme.getCertificateForDomain('example.com');
} catch (err) {
  if (err instanceof AcmeError) {
    console.log(err.status);        // HTTP status code (e.g. 429)
    console.log(err.type);          // ACME error URN (e.g. 'urn:ietf:params:acme:error:rateLimited')
    console.log(err.detail);        // Human-readable message
    console.log(err.subproblems);   // Per-identifier sub-errors (RFC 8555 §6.7.1)
    console.log(err.retryAfter);    // Retry-After value in seconds
    console.log(err.isRateLimited); // true for 429 or rateLimited type
    console.log(err.isRetryable);   // true for 429, 503, 5xx, badNonce; false for 403/404/409
  }
}

The built-in retry logic is error-aware: non-retryable errors (403, 404, 409) are thrown immediately without wasting retry attempts, and rate-limited responses respect the server's Retry-After header instead of using blind exponential backoff.

Domain Matching

SmartAcme automatically maps subdomains to their base domain for certificate lookups:

subdomain.example.com → certificate for example.com  ✅
*.example.com         → certificate for example.com  ✅
a.b.example.com       → not supported (4+ levels)    ❌

Environment

| Environment | Description | |----------------|-------------| | production | Let's Encrypt production servers. Certificates are browser-trusted. Rate limits apply. | | integration | Let's Encrypt staging servers. No rate limits, but certificates are not browser-trusted. Use for testing. |

Complete Example with HTTP-01

import { SmartAcme, certmanagers, handlers } from '@push.rocks/smartacme';
import * as http from 'http';

// In-memory handler for HTTP-01 challenges
const memHandler = new handlers.Http01MemoryHandler();

// Create HTTP server that serves ACME challenges
const server = http.createServer((req, res) => {
  memHandler.handleRequest(req, res, () => {
    res.statusCode = 200;
    res.end('OK');
  });
});
server.listen(80);

// Set up SmartAcme with in-memory storage and HTTP-01
const smartAcme = new SmartAcme({
  accountEmail: '[email protected]',
  certManager: new certmanagers.MemoryCertManager(),
  environment: 'production',
  challengeHandlers: [memHandler],
  challengePriority: ['http-01'],
});

await smartAcme.start();

const cert = await smartAcme.getCertificateForDomain('example.com');
// Use cert.publicKey and cert.privateKey with your HTTPS server

await smartAcme.stop();
server.close();

🏗️ ACME Directory Server (Built-in CA)

SmartAcme includes a full RFC 8555-compliant ACME Directory Server, allowing you to run your own Certificate Authority. This is useful for internal PKI, development/testing environments, and air-gapped networks.

Quick Start — ACME Server

import { server } from '@push.rocks/smartacme';

const acmeServer = new server.AcmeServer({
  port: 14000,
  challengeVerification: false, // Auto-approve challenges (for testing)
  caOptions: {
    commonName: 'My Internal CA',
    certValidityDays: 365,
  },
});

await acmeServer.start();
console.log(acmeServer.getDirectoryUrl()); // http://localhost:14000/directory
console.log(acmeServer.getCaCertPem());    // Root CA certificate in PEM format

// ... use it, then shut down
await acmeServer.stop();

Server Options

interface IAcmeServerOptions {
  port?: number;                    // Default: 14000
  hostname?: string;                // Default: '0.0.0.0'
  baseUrl?: string;                 // Auto-built from hostname:port if not provided
  challengeVerification?: boolean;  // Default: true. Set false to auto-approve challenges
  caOptions?: {
    commonName?: string;            // CA subject CN (default: 'SmartACME Test CA')
    validityDays?: number;          // Root cert validity in days (default: 3650)
    certValidityDays?: number;      // Issued cert validity in days (default: 90)
  };
}

Using the Server with the Low-Level ACME Client

The SmartAcme class connects to Let's Encrypt by default. To use a custom ACME directory (like your own server), use the lower-level AcmeClient directly:

import { server } from '@push.rocks/smartacme';
import { AcmeCrypto, AcmeClient } from '@push.rocks/smartacme/ts/acme/index.js';

// 1. Start your own CA
const acmeServer = new server.AcmeServer({
  port: 14000,
  challengeVerification: false, // auto-approve for testing
});
await acmeServer.start();

// 2. Create an ACME client pointing at your CA
const accountKey = AcmeCrypto.createRsaPrivateKey();
const client = new AcmeClient({
  directoryUrl: acmeServer.getDirectoryUrl(),
  accountKeyPem: accountKey,
});

// 3. Register an account
await client.createAccount({ termsOfServiceAgreed: true, contact: ['mailto:[email protected]'] });

// 4. Create an order and issue a certificate
const order = await client.createOrder({
  identifiers: [{ type: 'dns', value: 'myapp.internal' }],
});

// ... complete challenges, finalize, and download cert
// (challenges auto-approved since challengeVerification is false)

await acmeServer.stop();

Server Endpoints

The ACME server implements all RFC 8555 endpoints:

| Endpoint | Method | Description | |----------|--------|-------------| | /directory | GET | ACME directory with all endpoint URLs | | /new-nonce | HEAD/GET | Fresh replay nonce | | /new-account | POST | Account registration/lookup | | /new-order | POST | Create certificate order | | /order/:id | POST | Poll order status | | /authz/:id | POST | Get authorization with challenges | | /challenge/:id | POST | Trigger or poll challenge validation | | /finalize/:id | POST | Submit CSR and issue certificate | | /cert/:id | POST | Download PEM certificate chain |

Challenge Verification

By default, the server performs real challenge verification (HTTP-01 fetches the token, DNS-01 queries TXT records). Set challengeVerification: false to auto-approve all challenges — useful for testing or internal environments where domain validation isn't needed.

Root CA Certificate

Use getCaCertPem() to retrieve the root CA certificate for trust configuration:

import * as fs from 'fs';
fs.writeFileSync('/usr/local/share/ca-certificates/my-ca.crt', acmeServer.getCaCertPem());
// Then: sudo update-ca-certificates

🏛️ Architecture

Under the hood, SmartAcme uses a fully custom RFC 8555-compliant ACME protocol implementation (no external ACME libraries). Key internal modules:

Client Modules (ts/acme/)

| Module | Purpose | |--------|---------| | AcmeClient | Top-level ACME facade — orders, authorizations, finalization | | AcmeCrypto | RSA key generation, JWK/JWS (RFC 7515/7638), CSR via @peculiar/x509 | | AcmeHttpClient | JWS-signed HTTP transport with nonce management and structured logging | | AcmeError | Structured error class with type URN, subproblems, Retry-After, retryability | | AcmeOrderManager | Order lifecycle — create, poll, finalize, download certificate | | AcmeChallengeManager | Key authorization computation and challenge completion | | TaskManager | Constraint-based concurrency control, rate limiting, and request deduplication via @push.rocks/taskbuffer |

Server Modules (ts_server/)

| Module | Purpose | |--------|---------| | AcmeServer | Top-level server facade — start, stop, configuration | | AcmeServerCA | Self-signed root CA generation and certificate signing via @peculiar/x509 | | JwsVerifier | JWS signature verification (inverse of AcmeCrypto.createJws) | | NonceManager | Single-use replay nonce generation and validation | | ChallengeVerifier | HTTP-01 and DNS-01 challenge verification (with bypass mode) | | AcmeRouter | Minimal HTTP router with parameterized path support | | MemoryAccountStore | In-memory ACME account storage | | MemoryOrderStore | In-memory order, authorization, challenge, and certificate storage |

All cryptographic operations use node:crypto. The only external crypto dependency is @peculiar/x509 for CSR generation and certificate signing.

License and Legal Information

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at [email protected].

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.