acme-love
v2.1.0
Published
๐ Modern TypeScript ACME client & CLI for Let's Encrypt certificates. Supports DNS-01/HTTP-01 validation, wildcard domains, EAB (ZeroSSL/Google), nonce pooling. RFC 8555 compliant. Node.js 20+
Maintainers
Keywords
Readme
๐ ACME Love
Modern, stronglyโtyped ACME (RFC 8555) toolkit for Node.js 20+
Powerful CLI tool + TypeScript library for Let's Encrypt and other ACME Certificate Authorities
๐ Table of Contents
Main
- Table of Contents
- Key Features
- Quick Start
- Library Usage
- Nonce Management
- Advanced Validators & Utilities
- CSR Generation
- Supported ACME Providers
- Client Initialization
- CLI Features Showcase
- Documentation
- Troubleshooting
- Requirements
- Performance & Stress Testing
- Test Coverage
- License
- Contributing
โจ Key Features
| Feature | Description | | ------------------------------- | --------------------------------------------------------------------- | | ๐ฅ๏ธ Powerful CLI | Interactive & non-interactive modes with polished prompts | | ๐ Multi-Environment | Staging, production & custom directory endpoints | | ๐ Challenge Support | DNS-01 (wildcard-friendly) & HTTP-01 with built-in validation helpers | | ๐ Crypto Algorithms | ECDSA P-256 / P-384 / P-521 and RSA 2048 / 3072 / 4096 | | ๐ External Account Binding | EAB support (ZeroSSL, Google Trust Services, etc.) | | ๐ ๏ธ Resilient Error Handling | RFC 8555 problem+json mapping, retry & maintenance detection | | โก Optimized Core | Nonce pooling, FS read minimization, undici HTTP client | | ๐ข Multiple CAs | Presets: Let's Encrypt, Buypass, Google, ZeroSSL + custom URLs | | ๐ฆ Typed API | Strong TypeScript types for orders, accounts, directory metadata | | ๐ Diagnostics | Namespaced debug logging; stress & performance reports | | ๐งช Tested | Unit + stress tests; concurrency & rate limit scenarios | | ๐ง Developer Friendly | CSR helpers, validation utilities, composable modules |
๐ Quick Start
CLI Installation & Usage
๐ Full CLI Documentation: See CLI.md for complete usage guide, examples, and advanced features.
# Global installation (recommended)
npm install -g acme-love
acme-love --help
# Or use without installation
npx acme-love interactive --staging๐ฎ Interactive Mode (Easiest Way)
# Start interactive mode with environment selection
acme-love interactive
# Or with pre-selected environment
acme-love interactive --staging # For testing
acme-love interactive --production # For real certificates๐ Command Line Mode
# Get a staging certificate (recommended first)
acme-love cert \
--domain test.acme-love.com \
--email [email protected] \
--staging \
--challenge dns-01
# Get a production certificate with custom algorithms
acme-love cert \
--domain acme-love.com \
--email [email protected] \
--production \
--challenge http-01 \
--account-algo ec-p256 \
--cert-algo rsa-4096
# Create an account key with specific algorithm
acme-love create-account-key \
--algo ec-p384 \
--output ./my-account-key.json
# Use External Account Binding for commercial CAs
acme-love cert \
--domain acme-love.com \
--email [email protected] \
--directory https://acme.zerossl.com/v2/DV90 \
--eab-kid "your-key-identifier" \
--eab-hmac-key "your-base64url-hmac-key"๐ฏ Challenge Types
DNS-01 Challenge (Recommended)
acme-love cert --challenge dns-01 --domain acme-love.com --email [email protected] --staging- โ
Works with wildcard certificates (
*.acme-love.com) - โ No need for public web server
- ๐ง Requires DNS provider access
HTTP-01 Challenge
acme-love cert --challenge http-01 --domain acme-love.com --email [email protected] --staging- โ Simple validation via HTTP file
- โ Automatic validation with built-in checker
- ๐ง Requires domain to point to your web server
๐ Cryptographic Algorithms
The CLI uses P-256 ECDSA by default for both account and certificate keys, providing an excellent balance of security and performance. This algorithm is:
- โ Fast: Quicker than RSA for signing operations
- โ Secure: 256-bit elliptic curve equivalent to 3072-bit RSA
- โ Compatible: Widely supported by browsers and servers
- โ Compact: Smaller key sizes and certificate files
For programmatic usage via the library, you can choose from multiple algorithms including different ECDSA curves (P-256, P-384, P-521) and RSA key sizes (2048, 3072, 4096 bits). See the Supported Cryptographic Algorithms section for details.
๐ ๏ธ Development & Local Usage
If you're developing or testing locally, you have multiple convenient options:
# Development wrapper (auto-builds when needed)
./acme-love --help
# NPM scripts
npm run cli:help
npm run cli:staging
npm run cli:production
# Make commands
make help
make interactive
make stagingSee CLI.md for complete CLI documentation and usage examples.
๐ CLI Commands Reference
| Command | Purpose | Algorithm Options | EAB Options |
| -------------------- | ------------------------------ | ------------------------------------ | ------------------------------------------ |
| cert | Obtain SSL certificate | --account-algo, --cert-algo | --eab-kid, --eab-hmac-key |
| create-account-key | Generate ACME account key | --algo | - |
| interactive | Interactive certificate wizard | Full interactive algorithm selection | Prompts for EAB when custom directory used |
Algorithm Values: ec-p256 (default), ec-p384, ec-p521, rsa-2048, rsa-3072, rsa-4096
EAB Options: --eab-kid <identifier>, --eab-hmac-key <base64url-key> for commercial CAs
Examples:
# Generate P-384 ECDSA account key
acme-love create-account-key --algo ec-p384 --output ./my-account.json
# Mixed algorithms: P-256 account, RSA-4096 certificate
acme-love cert --account-algo ec-p256 --cert-algo rsa-4096 --domain acme-love.com
# Interactive mode with full algorithm selection
acme-love interactive --staging
# Commercial CA with External Account Binding
acme-love cert \
--domain acme-love.com \
--directory https://acme.zerossl.com/v2/DV90 \
--eab-kid "your-eab-key-id" \
--eab-hmac-key "your-eab-hmac-key"
# Note: For known CAs, you can also use predefined providers in the library:
# AcmeClient(provider.zerossl.production) or AcmeClient(provider.google.production)๐ Library Usage
Installation
npm install acme-loveModern ACME Client
import { AcmeClient, AcmeAccount, provider, generateKeyPair, createAcmeCsr } from 'acme-love';
// 1. Create ACME client - using provider preset (recommended)
const client = new AcmeClient(provider.letsencrypt.staging, {
nonce: { maxPool: 64 },
});
// Alternative: Create client with string URL
// const client = new AcmeClient(provider.letsencrypt.staging.directoryUrl, {
// nonce: { maxPool: 64 },
// });
// 2. Generate account keys (P-256 ECDSA recommended)
const algo = { kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' };
const keyPair = await generateKeyPair(algo);
const accountKeys = {
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey,
};
// 3. Create account
const account = new AcmeAccount(client, accountKeys);
// 4. Register account
const registration = await account.register({
contact: '[email protected]', // string or array
termsOfServiceAgreed: true,
});
// 5. Create order and solve challenges
const order = await account.createOrder(['acme-love.com']);
// DNS-01 challenge
const ready = await account.solveDns01(order, {
setDns: async (preparation) => {
console.log(`Create TXT record: ${preparation.target} = ${preparation.value}`);
// Set DNS record via your DNS provider API
await waitForUserConfirmation();
},
waitFor: async (preparation) => {
// Validate DNS propagation
console.log('Validating DNS...');
},
});
// 6. Generate CSR and finalize
const { derBase64Url, keys: csrKeys } = await createAcmeCsr(['acme-love.com'], algo);
const finalized = await account.finalize(ready, derBase64Url);
const valid = await account.waitOrder(finalized, ['valid']);
const certificate = await account.downloadCertificate(valid);
console.log('Certificate obtained!', certificate);External Account Binding (EAB) Support
For Certificate Authorities that require External Account Binding (like ZeroSSL, Google Trust Services), provide EAB credentials during account registration:
import { AcmeClient, AcmeAccount, generateKeyPair, provider } from 'acme-love';
// Create client for CA that requires EAB - using provider preset (recommended)
const client = new AcmeClient(provider.zerossl.production);
// Alternative: Create client with string URL
// const client = new AcmeClient('https://acme.zerossl.com/v2/DV90');
// Generate account keys
const keyPair = await generateKeyPair({ kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' });
const accountKeys = {
privateKey: keyPair.privateKey!,
publicKey: keyPair.publicKey!,
};
// Create account with EAB
const account = new AcmeAccount(client, accountKeys, {
externalAccountBinding: {
kid: 'your-key-identifier-from-ca', // Provided by your CA
hmacKey: 'your-base64url-hmac-key-from-ca', // Provided by your CA
},
});
// Register account (EAB is handled automatically)
const registration = await account.register({
contact: '[email protected]',
termsOfServiceAgreed: true,
});
// Continue with normal certificate issuance...
const order = await account.createOrder(['acme-love.com']);
// ... rest of the flowEAB-enabled Certificate Authorities:
- ZeroSSL: Requires EAB for new account registration
- Google Trust Services: Requires EAB for all accounts
- Buypass: Optional EAB for enhanced validation
- Custom/Enterprise CAs: Many require EAB for access control
Getting EAB Credentials:
- Register with your chosen CA's website
- Navigate to ACME/API settings in your account dashboard
- Generate or retrieve your EAB Key ID and HMAC Key
- Use these credentials in your ACME client configuration
CLI Usage with EAB:
# ZeroSSL example
acme-love cert \
--domain acme-love.com \
--eab-hmac-key "your-zerossl-hmac-key"
# Google Trust Services example
acme-love cert \
--domain acme-love.com \
--eab-hmac-key "your-google-hmac-key"๐ Detailed EAB documentation: docs/EAB.md
Supported Cryptographic Algorithms
ACME Love supports multiple cryptographic algorithms for both account keys and certificate keys:
ECDSA (Elliptic Curve Digital Signature Algorithm)
- P-256 (recommended) - Fast, secure, widely supported
- P-384 - Higher security, larger keys
- P-521 - Maximum security, largest keys
// P-256 (recommended for most cases)
const p256Algo = { kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' };
// P-384 for enhanced security
const p384Algo = { kind: 'ec', namedCurve: 'P-384', hash: 'SHA-384' };
// P-521 for maximum security
const p521Algo = { kind: 'ec', namedCurve: 'P-521', hash: 'SHA-512' };RSA (Rivest-Shamir-Adleman)
- 2048-bit - Minimum supported, fast
- 3072-bit - Enhanced security
- 4096-bit - Maximum security, slower
// RSA 2048-bit (minimum)
const rsa2048Algo = { kind: 'rsa', modulusLength: 2048, hash: 'SHA-256' };
// RSA 3072-bit (enhanced security)
const rsa3072Algo = { kind: 'rsa', modulusLength: 3072, hash: 'SHA-256' };
// RSA 4096-bit (maximum security)
const rsa4096Algo = { kind: 'rsa', modulusLength: 4096, hash: 'SHA-384' };Algorithm Selection Guidelines
- P-256 ECDSA: Default choice, excellent performance/security balance
- P-384/P-521 ECDSA: For compliance requirements or enhanced security
- RSA 2048: Legacy compatibility, larger certificate size
- RSA 3072/4096: High-security environments, significantly slower
Important: You can use different algorithms for account keys and certificate keys independently. Account keys are used for ACME protocol authentication, while certificate keys are embedded in the final TLS certificate.
TypeScript Types: The library exports AcmeEcAlgorithm, AcmeRsaAlgorithm, and their union AcmeCertificateAlgorithm for type-safe algorithm specification:
import type { AcmeEcAlgorithm, AcmeRsaAlgorithm, AcmeCertificateAlgorithm } from 'acme-love';
// Type-safe algorithm definitions
const ecAlgo: AcmeEcAlgorithm = { kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' };
const rsaAlgo: AcmeRsaAlgorithm = { kind: 'rsa', modulusLength: 2048, hash: 'SHA-256' };
const algo: AcmeCertificateAlgorithm = ecAlgo; // Union typeExample: Different Algorithms for Account and Certificate
import { generateKeyPair, createAcmeCsr } from 'acme-love';
// Use P-256 for account keys (fast ACME protocol operations)
const accountAlgo = { kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' };
const accountKeys = await generateKeyPair(accountAlgo);
// Use P-384 for certificate keys (enhanced security in final certificate)
const certAlgo = { kind: 'ec', namedCurve: 'P-384', hash: 'SHA-384' };
const { derBase64Url, keys: certKeys } = await createAcmeCsr(['acme-love.com'], certAlgo);
// Or mix ECDSA and RSA
const accountAlgo = { kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' }; // Fast ECDSA for account
const certAlgo = { kind: 'rsa', modulusLength: 4096, hash: 'SHA-256' }; // RSA for certificate compatibilityWorking with Existing Accounts
When you already have a registered ACME account, you can reuse it by providing the kid (Key ID) to avoid creating duplicate registrations:
// First time: Register new account and save the registration details
const account = new AcmeAccount(client, accountKeys);
const registration = await account.register({
contact: '[email protected]',
termsOfServiceAgreed: true,
});
// Save the account URL (kid) for future use
const accountUrl = registration.accountUrl;
// Save account info for future use
// Save account information for reuse
const accountInfo = {
accountUrl, // This is the kid
privateKey: await crypto.subtle.exportKey('jwk', accountKeys.privateKey),
publicKey: await crypto.subtle.exportKey('jwk', accountKeys.publicKey),
};
await fs.writeFile('account.json', JSON.stringify(accountInfo, null, 2));// Later: Load existing account with kid
const savedAccount = JSON.parse(await fs.readFile('account.json', 'utf8'));
const accountKeys = {
privateKey: await crypto.subtle.importKey(
'jwk',
savedAccount.privateKey,
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['sign'],
),
publicKey: await crypto.subtle.importKey(
'jwk',
savedAccount.publicKey,
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['verify'],
),
};
// Create account with existing account URL (kid)
const account = new AcmeAccount(client, accountKeys, {
kid: savedAccount.accountUrl, // Use existing account URL
});
// No need to call register() - account already exists
const order = await account.createOrder(['acme-love.com']);Advanced Features
Comprehensive Error Handling
ACME Love provides detailed error types for precise error handling in your applications. All ACME-specific errors extend the base AcmeError class and follow RFC 8555 error specifications.
ACME Protocol Errors
import {
AcmeError,
ServerMaintenanceError,
RateLimitedError,
BadNonceError,
AccountDoesNotExistError,
OrderNotReadyError,
ExternalAccountRequiredError,
RateLimitError, // From rate limiter
} from 'acme-love';
try {
await account.createOrder(['acme-love.com']);
} catch (error) {
// Server maintenance detection
if (error instanceof ServerMaintenanceError) {
console.log('๐ง Service is under maintenance');
console.log('๐ Check https://letsencrypt.status.io/');
console.log('โณ Please try again later when the service is restored.');
return;
}
// Rate limiting with automatic retry information
if (error instanceof RateLimitedError) {
const retrySeconds = error.getRetryAfterSeconds();
console.log(`โฑ๏ธ Rate limited. Retry in ${retrySeconds} seconds`);
console.log(`๐ Details: ${error.detail}`);
// Wait and retry automatically
if (retrySeconds && retrySeconds < 300) {
// Max 5 minutes
await new Promise((resolve) => setTimeout(resolve, retrySeconds * 1000));
// Retry the operation...
}
return;
}
// Nonce issues (automatically handled by nonce manager)
if (error instanceof BadNonceError) {
console.log('๐ Invalid nonce - this should be handled automatically');
// The NonceManager typically retries these automatically
}
// Account-related errors
if (error instanceof AccountDoesNotExistError) {
console.log('๐ค Account does not exist - need to register first');
}
// Order state errors
if (error instanceof OrderNotReadyError) {
console.log('๐ Order not ready for finalization - complete challenges first');
}
// EAB requirement
if (error instanceof ExternalAccountRequiredError) {
console.log('๐ This CA requires External Account Binding (EAB)');
console.log('๐ก Use --eab-hmac-key option or provide EAB credentials');
}
// Rate limiter errors (from internal rate limiting system)
if (error instanceof RateLimitError) {
console.log(`๐ฆ Internal rate limit: ${error.message}`);
console.log(`๐ Attempts: ${error.rateLimitInfo.attempts}`);
console.log(`โณ Retry in ${error.rateLimitInfo.retryDelaySeconds}s`);
}
// Generic ACME error with details
if (error instanceof AcmeError) {
console.log(`โ ACME Error: ${error.detail}`);
console.log(`๐ Type: ${error.type}`);
console.log(`๐ Status: ${error.status}`);
// Handle subproblems for compound errors
if (error.subproblems?.length) {
console.log('๐ Subproblems:');
error.subproblems.forEach((sub, i) => {
console.log(` ${i + 1}. ${sub.detail} (${sub.type})`);
});
}
}
}Complete Error Types Reference
Account & Registration Errors:
AccountDoesNotExistError- Account doesn't exist, need to registerExternalAccountRequiredError- CA requires EAB for registrationInvalidContactError- Invalid contact information (email format, etc.)UnsupportedContactError- Unsupported contact protocol scheme
Authentication & Authorization Errors:
BadNonceError- Invalid anti-replay nonce (auto-retried by NonceManager)BadPublicKeyError- Unsupported public key algorithmBadSignatureAlgorithmError- Unsupported signature algorithmUnauthorizedError- Insufficient authorization for requested actionUserActionRequiredError- Manual action required (visit instance URL)
Certificate & CSR Errors:
BadCSRError- Invalid Certificate Signing RequestBadRevocationReasonError- Invalid revocation reason providedAlreadyRevokedError- Certificate already revokedOrderNotReadyError- Order not ready for finalizationRejectedIdentifierError- Server won't issue for this identifier
Validation Errors:
CAAError- CAA DNS records forbid certificate issuanceConnectionError- Can't connect to validation targetDNSError- DNS resolution problems during validationIncorrectResponseError- Challenge response doesn't match requirementsTLSError- TLS issues during validationUnsupportedIdentifierError- Unsupported identifier type
Server & Network Errors:
ServerInternalError- Internal server error (500)ServerMaintenanceError- Service under maintenance (503)RateLimitedError- ACME rate limit exceeded with retry infoMalformedError- Malformed request messageCompoundError- Multiple errors (check subproblems array)
Rate Limiting Errors:
RateLimitError- Internal rate limiter exceeded retry attempts
Error Handling Patterns
Pattern 1: Graceful Degradation
async function createCertificateWithFallback(domain: string) {
try {
return await createCertificate(domain);
} catch (error) {
if (error instanceof RateLimitedError) {
// Wait and retry once
const retrySeconds = error.getRetryAfterSeconds();
if (retrySeconds && retrySeconds < 3600) {
// Max 1 hour
await new Promise((resolve) => setTimeout(resolve, retrySeconds * 1000));
return await createCertificate(domain);
}
}
if (error instanceof ServerMaintenanceError) {
// Try staging environment as fallback
return await createCertificateStaging(domain);
}
throw error; // Re-throw unhandled errors
}
}Pattern 2: Retry with Exponential Backoff
async function robustCertificateCreation(domain: string, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await createCertificate(domain);
} catch (error) {
attempt++;
if (error instanceof BadNonceError && attempt < maxRetries) {
// Exponential backoff for nonce errors
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
if (error instanceof RateLimitedError) {
const retrySeconds = error.getRetryAfterSeconds();
if (retrySeconds && retrySeconds < 1800) {
// Max 30 minutes
await new Promise((resolve) => setTimeout(resolve, retrySeconds * 1000));
continue;
}
}
// Don't retry these errors
if (
error instanceof ExternalAccountRequiredError ||
error instanceof BadCSRError ||
error instanceof RejectedIdentifierError
) {
throw error;
}
if (attempt >= maxRetries) throw error;
}
}
}Pattern 3: Error Categorization
function categorizeError(error: unknown): 'retry' | 'reconfigure' | 'fatal' {
if (
error instanceof RateLimitedError ||
error instanceof BadNonceError ||
error instanceof ServerMaintenanceError ||
error instanceof ConnectionError
) {
return 'retry';
}
if (
error instanceof ExternalAccountRequiredError ||
error instanceof BadCSRError ||
error instanceof InvalidContactError ||
error instanceof UnsupportedContactError
) {
return 'reconfigure';
}
return 'fatal';
}
async function handleCertificateError(error: unknown) {
const category = categorizeError(error);
switch (category) {
case 'retry':
console.log('โณ Temporary issue - will retry automatically');
break;
case 'reconfigure':
console.log('โ๏ธ Configuration issue - please check your settings');
break;
case 'fatal':
console.log('โ Fatal error - manual intervention required');
break;
}
}JSON Serialization
All ACME errors support JSON serialization for logging and debugging:
try {
await acct.createOrder(['acme-love.com']);
} catch (error) {
if (error instanceof AcmeError) {
// Structured error logging
const errorData = error.toJSON();
console.log('ACME Error Details:', JSON.stringify(errorData, null, 2));
// Send to monitoring system
await sendToMonitoring({
event: 'acme_error',
error: errorData,
timestamp: new Date().toISOString(),
});
}
}HTTP-01 Challenge with Validation
const ready = await acct.solveHttp01(order, {
setHttp: async (preparation) => {
// Serve challenge at: preparation.target
// Content: preparation.value
console.log(`Serve ${preparation.value} at ${preparation.target}`);
},
waitFor: async (preparation) => {
// Built-in HTTP validator
const { validateHttp01ChallengeByUrl } = await import('acme-love/validator');
const result = await validateHttp01ChallengeByUrl(preparation.target, preparation.value);
if (!result.ok) throw new Error('HTTP validation failed');
},
});Custom Cryptographic Algorithms
import { generateKeyPair, createAcmeCsr } from 'acme-love';
// High-security setup: P-521 for account, RSA-4096 for certificate
const accountAlgo = { kind: 'ec', namedCurve: 'P-521', hash: 'SHA-512' };
const certAlgo = { kind: 'rsa', modulusLength: 4096, hash: 'SHA-384' };
const accountKeys = await generateKeyPair(accountAlgo);
const { derBase64Url, keys: certKeys } = await createAcmeCsr(['acme-love.com'], certAlgo);
// Performance-optimized setup: P-256 for both
const fastAlgo = { kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' };
const accountKeys = await generateKeyPair(fastAlgo);
const { derBase64Url } = await createAcmeCsr(['acme-love.com'], fastAlgo);Debug Logging for Library Usage
import { enableDebug, debugNonce, debugHttp } from 'acme-love';
// Enable debug for development
enableDebug();
// Use specific debug loggers in your code
debugNonce('Custom nonce debug message');
debugHttp('HTTP operation debug info');โก Nonce Management
ACME Love includes a sophisticated NonceManager that optimizes nonce handling for high-performance certificate operations. Nonces are automatically pooled, prefetched, and recycled to minimize network round-trips.
Global Configuration
Set default nonce behavior for all accounts:
const client = new AcmeClient(provider.letsencrypt.production, {
nonce: {
maxPool: 64, // Cache up to 64 nonces
prefetchLowWater: 12, // Start prefetch when < 12 remain
prefetchHighWater: 40, // Fill up to 40 nonces
maxAgeMs: 5 * 60_000, // Expire after 5 minutes
// Note: Logging is handled by unified debug system (DEBUG=acme-love:nonce)
},
});Per-Account Overrides
Fine-tune nonce behavior for specific accounts:
const account = new AcmeAccount(client, accountKeys, {
nonceOverrides: {
maxPool: 128, // Higher throughput for this account
prefetchLowWater: 20,
prefetchHighWater: 80,
// Note: Logging is handled by unified debug system (DEBUG=acme-love:nonce)
},
});Configuration Options
| Option | Default | Description |
| ------------------- | ------- | -------------------------------------------------- |
| maxPool | 32 | Maximum cached nonces per namespace |
| prefetchLowWater | 0 | Start prefetch when pool below this (0 = disabled) |
| prefetchHighWater | 0 | Target fill level for prefetch |
| maxAgeMs | 300000 | Discard nonces older than 5 minutes |
Note: Logging is now handled by the unified debug system. Use DEBUG=acme-love:nonce to enable nonce manager debug output.
Performance Scenarios
// Low traffic / sequential operations
{ prefetchLowWater: 0 } // Disable prefetch
// Moderate parallelism (5-10 concurrent operations)
{ prefetchLowWater: 4, prefetchHighWater: 12, maxPool: 32 }
// High burst / parallel workloads
{ prefetchLowWater: 16, prefetchHighWater: 48, maxPool: 128 }The NonceManager automatically handles badNonce retries, harvests nonces from response headers, and isolates nonce pools per CA/account combination.
Debug Logging
ACME Love uses the debug module for structured logging. Debug output is automatically disabled in production unless explicitly enabled:
# Enable all ACME Love debug output
DEBUG=acme-love:* node your-app.js
# Enable only nonce manager debug
DEBUG=acme-love:nonce node your-app.js
# Enable multiple specific components
DEBUG=acme-love:nonce,acme-love:http node your-app.js
# Available debug namespaces:
# acme-love:nonce - Nonce management operations
# acme-love:http - HTTP requests and responses
# acme-love:challenge - Challenge solving process
# acme-love:client - Core client operations
# acme-love:validator - Validation functions// Programmatic debug control
import { enableDebug, disableDebug, isDebugEnabled } from 'acme-love';
// Enable debug programmatically (useful for development)
enableDebug();
// Disable debug programmatically (useful for production)
disableDebug();
// Check if debug is enabled
if (isDebugEnabled()) {
console.log('Debug logging is active');
}Custom Nonce Manager Logging
In previous versions, NonceManager accepted a custom log function for debugging. This has been replaced with the unified debug system. If you need custom logging behavior for nonce operations, you can intercept the debug output:
// New unified approach:
import debug from 'debug';
// Override the nonce debug function for custom formatting
const originalNonceDebug = debug('acme-love:nonce');
debug.enabled = () => true; // Force enable for custom handler
// Custom nonce logging with your preferred logger
const customNonceLogger = (...args: any[]) => {
logger.info('[nonce]', ...args); // Your custom logger
originalNonceDebug(...args); // Still call original if needed
};
// Replace the debug function globally
debug('acme-love:nonce').log = customNonceLogger;๐ Detailed documentation: docs/NONCE-MANAGER.md
๐ Advanced Validators & Utilities
DNS Validation Functions
For advanced DNS-01 challenge handling, ACME Love provides powerful DNS validation utilities:
import {
resolveAndValidateAcmeTxtAuthoritative,
resolveAndValidateAcmeTxt,
resolveNsToIPs,
findZoneWithNs,
} from 'acme-love/validator';
// Authoritative DNS validation (queries actual NS servers)
const result = await resolveAndValidateAcmeTxtAuthoritative(
'_acme-challenge.acme-love.com',
'expected-challenge-value',
);
if (result.ok) {
console.log('โ
DNS challenge validated');
} else {
console.log('โ DNS validation failed:', result.error);
}
// Standard DNS validation with fallback to public resolvers
const quickResult = await resolveAndValidateAcmeTxt(
'_acme-challenge.acme-love.com',
'expected-challenge-value',
);HTTP Validation Functions
import { validateHttp01ChallengeByUrl, validateHttp01Challenge } from 'acme-love/validator';
// Direct URL validation
const result = await validateHttp01ChallengeByUrl(
'http://acme-love.com/.well-known/acme-challenge/token123',
'expected-key-authorization',
);
// Domain + token validation
const result2 = await validateHttp01Challenge(
'acme-love.com',
'token123',
'expected-key-authorization',
);CLI Configuration Details
When using the CLI, ACME Love automatically handles file organization and configuration:
# Default directory structure created by CLI
./certificates/
โโโ acme-love.com/
โ โโโ cert.pem # Certificate chain
โ โโโ cert-key.json # Certificate private key (JWK format)
โ โโโ cert.csr.pem # Certificate signing request
โ โโโ order.json # ACME order details
โโโ account-key.json # ACME account keys (JWK format)CLI Configuration Defaults:
- Output directory:
./certificates/ - Account key path:
./certificates/account-key.json - Nonce pool size: 64 (optimized for CLI operations)
- Key algorithm: ECDSA P-256 (ES256)
- File format: JWK for keys, PEM for certificates
๐ง CSR Generation
The createAcmeCsr helper generates everything needed for certificate finalization:
import { createAcmeCsr } from 'acme-love';
const { pem, derBase64Url, keys } = await createAcmeCsr(
['acme-love.com', 'www.acme-love.com'], // domains (first = CN)
{ kind: 'ec', namedCurve: 'P-256', hash: 'SHA-256' },
);
// pem: PEM-encoded CSR for storage
// derBase64Url: base64url DER for ACME finalize
// keys: Certificate private/public key pairSupported Cryptographic Algorithms
ACME Love supports modern cryptographic algorithms via WebCrypto API:
import { generateKeyPair, type AcmeCertificateAlgorithm } from 'acme-love';
// ECDSA with P-256 curve (Recommended - smaller keys, faster)
const ecAlgo: AcmeCertificateAlgorithm = {
kind: 'ec',
namedCurve: 'P-256',
hash: 'SHA-256',
};
// ECDSA with P-384 curve (Higher security)
const ec384Algo: AcmeCertificateAlgorithm = {
kind: 'ec',
namedCurve: 'P-384',
hash: 'SHA-384',
};
// RSA 2048-bit (Widely compatible, larger keys)
const rsaAlgo: AcmeCertificateAlgorithm = {
kind: 'rsa',
modulusLength: 2048,
hash: 'SHA-256',
};
// RSA 4096-bit (Maximum security, slower)
const rsa4096Algo: AcmeCertificateAlgorithm = {
kind: 'rsa',
modulusLength: 4096,
hash: 'SHA-256',
};
// Generate key pair with chosen algorithm
const keyPair = await generateKeyPair(ecAlgo);Algorithm Recommendations:
- ECDSA P-256: Best balance of security, performance, and compatibility
- ECDSA P-384: For applications requiring higher security
- RSA 2048: For maximum compatibility with older systems
- RSA 4096: For maximum security (slower operations)
Key Storage Formats:
- JWK (JSON Web Key): Used for account keys and internal storage
- PEM: Standard format for certificate keys and CSRs
- DER: Binary format for ACME protocol communication
๐ข Supported ACME Providers
Built-in directory presets for major Certificate Authorities:
import { provider } from 'acme-love';
// Let's Encrypt (No EAB required)
provider.letsencrypt.staging.directoryUrl;
provider.letsencrypt.production.directoryUrl;
// Buypass (EAB optional)
provider.buypass.staging.directoryUrl;
provider.buypass.production.directoryUrl;
// Google Trust Services (EAB required)
provider.google.staging.directoryUrl;
provider.google.production.directoryUrl;
// ZeroSSL (EAB required for new accounts)
provider.zerossl.production.directoryUrl;EAB Requirements by Provider:
| Provider | EAB Required | Notes | | --------------------- | ------------ | --------------------------------------- | | Let's Encrypt | โ No | Free, automatic registration | | Buypass | โ ๏ธ Optional | Enhanced validation with EAB | | Google Trust Services | โ Required | Commercial service, register for access | | ZeroSSL | โ Required | Free tier available, EAB mandatory |
Use --eab-kid and --eab-hmac-key CLI options or the eab parameter in ensureRegistered() for providers that require External Account Binding.
๐ง Client Initialization
ACME Love supports two convenient ways to initialize the AcmeClient:
Method 1: Using Provider Presets (Recommended)
import { AcmeClient, provider } from 'acme-love';
// Using predefined provider entries (recommended)
const client = new AcmeClient(provider.letsencrypt.staging);
const client2 = new AcmeClient(provider.google.production);
const client3 = new AcmeClient(provider.zerossl.production);
// With configuration options
const client4 = new AcmeClient(provider.letsencrypt.production, {
nonce: { maxPool: 64 },
});Method 2: Using String URLs
import { AcmeClient } from 'acme-love';
// Using string URLs directly
const client = new AcmeClient('https://acme-staging-v02.api.letsencrypt.org/directory');
const client2 = new AcmeClient('https://dv.acme-v02.api.pki.goog/directory');
// Custom ACME directory
const client3 = new AcmeClient('https://my-custom-ca.com/acme/directory');Benefits of Provider Presets
โ Type Safety: Full TypeScript support with autocomplete โ Validation: Pre-validated directory URLs โ Convenience: No need to remember complex URLs โ Consistency: Standardized configuration across projects โ Updates: Automatic URL updates with library updates
Recommendation: Use provider presets for standard CAs (Let's Encrypt, Google, etc.) and string URLs for custom or enterprise ACME directories.
๐จ CLI Features Showcase
Beautiful Interactive Prompts
- ๐ฎ Full interactive mode with guided setup
- ๐ Colorful, emoji-rich interface using
@inquirer/prompts - ๐ง Environment selection (staging/production/custom)
- ๐ Challenge type selection (DNS-01/HTTP-01)
Smart Error Handling
- ๐ง Maintenance detection with helpful messages
- ๐ Links to service status pages
- ๐ก User-friendly error explanations
- ๐จ Proper exit codes
Automatic Validation
- ๐ DNS record verification with authoritative lookups
- ๐ HTTP challenge validation with
undici - โณ Retry logic with progress indicators
- โ Success confirmation
๐ Documentation
- CLI Documentation - Complete CLI usage guide and examples
- API Documentation - Library API reference
- Examples - Code examples and use cases
๐ง Troubleshooting
Common Issues
WebCrypto API Errors
# Error: crypto.subtle is undefined
# Solution: Ensure Node.js โฅ 20 and secure context (HTTPS/localhost)JWK Import/Export Issues
// โ Wrong algorithm specification
await crypto.subtle.importKey('jwk', jwkData, { name: 'ECDSA' }, false, ['sign']);
// โ
Correct algorithm with namedCurve
await crypto.subtle.importKey('jwk', jwkData, { name: 'ECDSA', namedCurve: 'P-256' }, false, [
'sign',
]);Debug Logging Control
# Production: Debug disabled by default (no DEBUG environment variable)
node your-app.js
# Development: Enable specific debug output
DEBUG=acme-love:nonce node your-app.js
# Troubleshooting: Enable all debug output
DEBUG=acme-love:* node your-app.js// Programmatic control for tests or specific environments
import { disableDebug } from 'acme-love';
// Ensure clean logs in production
if (process.env.NODE_ENV === 'production') {
disableDebug();
}DNS Challenge Validation Timeout
# If DNS propagation is slow, increase timeout in your waitFor function
await new Promise(resolve => setTimeout(resolve, 30000)); // Wait 30sFile Permission Errors
# Ensure write permissions for certificate output directory
chmod 755 ./certificates/โก Requirements
- Node.js โฅ 20 (WebCrypto, modern URL, base64url support)
- TypeScript โฅ 5 (for development)
๐ Performance & Stress Testing
ACME Love undergoes regular stress tests (Let's Encrypt staging) across multiple load tiers. Below are the latest consolidated results pulled from the stress report artifacts (Quick / Standard / Heavy). They demonstrate scalability, nonceโpool efficiency, and stability as order volume increases.
๐ข Consolidated Metrics (Latest Run)
| Tier | Accounts | Orders/Acc | Total Orders | Total Time | Avg Resp | P50 | P95 | P99 | Requests | Req/s | Orders/s | Success | New-Nonce | Nonce Eff. | Saved Req. | | ------------ | -------- | ---------- | ------------ | ---------- | -------- | --- | ---- | ---- | -------- | ----- | -------- | ------- | --------- | ---------- | ---------- | | Quick | 2 | 20 | 40 | 6.16s | 346ms | 239 | 741 | 821 | 93 | 15.1 | 6.50 | 100% | 13 | 86% | 80 | | Standard | 4 | 50 | 200 | 10.95s | 451ms | 250 | 1283 | 1630 | 450 | 41.1 | 18.27 | 100% | 50 | 89% | 400 | | Heavy | 4 | 200 | 800 | 32.79s | 494ms | 246 | 1338 | 1554 | 1652 | 50.4 | 24.40 | 100% | 52 | 97% | 1600 |
All tiers achieved 100% success. Heavy test maintains <500ms average latency and high nonce pool efficiency (97%).
๐งช Interpretation
- 100% Success Rate: All test scenarios (Quick, Standard, Heavy) completed without errors.
- Stable Performance: Average response time is <500ms even under heavy load (Heavy: 494ms, Standard: 451ms, Quick: 346ms).
- Scalability: Requests/sec and Orders/sec increase with load, latency does not degrade.
- Nonce Efficiency: Savings on new-nonce requests up to 97% (Heavy), significantly reducing load on the CA.
- No Errors: Error Rate = 0% in all tests.
- Good tail latency: p99 latency <1600ms even in Heavy tier, no abnormally slow requests.
- Resource margin: The system handles increased load without loss of stability or efficiency.
โ๏ธ Key Optimizations
- Rate limiting + exponential backoff (automatic HTTP 503 / Retry-After handling)
- Highโefficiency nonce pool (dynamic refill + minimal new-nonce calls)
- Request coalescing & HTTP connection reuse
- Structured debug logging (HTTP + nonce) via DEBUG env
๐ Example High-Load Configuration
const client = new AcmeClient(directoryUrl, {
nonce: {
maxPool: 64,
prefetchLowWater: 8,
prefetchHighWater: 32,
},
// Optional: tune timeouts / retry strategies as needed
});๐ Detailed Reports
Comprehensive performance & reliability artifacts (humanโreadable Markdown + machineโreadable JSON):
Primary stress tiers:
Each stress test report includes: latency distribution (P50/P75/P90/P95/P99), throughput, nonce efficiency, savings, threshold matrix, environment metadata (git commit, Node version), and raw config for reproducibility.
๐ Running the Tests
Test Types
- ๐ฌ Unit Tests: Mock-based testing of individual components
- ๐ Integration Tests: Real requests to Let's Encrypt staging
- โก Async Behavior Tests: Concurrent operations and memory leak prevention
- ๐ E2E Tests: Full workflow testing with staging environment
- ๐ Stress Tests: High-volume production scenario validation (run separately)
# Run standard test suite (fast, excludes stress tests)
npm test
# Run specific test types
npm run test:unit # Unit tests only (skips e2e & stress)
npm run test:e2e # End-to-end tests (Let's Encrypt staging)
npm run test:e2e:ci # E2E for CI (requires ACME_E2E_ENABLED=1)
# Run with coverage report
npm run test:coverage
# Focused component tests
npm run test:nonce-manager # Nonce manager focused tests (in-band)
npm run test:rate-limiting # Rate limiting behavior & backoff
npm run test:deadlock # Deadlock detection / concurrency safety
# Stress tiers (instrumented performance runs)
npm run test:quick # Quick tier (2 ร 20 orders) ~7s
npm run test:standard # Standard tier (4 ร 50 orders) ~16s
npm run test:heavy # Heavy tier (4 ร 200 orders) ~60s
npm run test:deadlock # Deadlock detection
# Full suite
npm run test:all # EVERYTHING (includes stress) โ slowerโ Resolution:
- Implemented comprehensive rate limiting system with HTTP 503 detection
- Added exponential backoff with Retry-After header support
- Optimized nonce pool management with 98% efficiency
- Unified debug logging system with printf-style formatting
- Latest test results: 4 accounts ร 200 orders = 800 operations completed successfully in 32 seconds
Current Status:
- โ 100% success rate in heavy stress testing
- โ Zero rate limit violations with automatic backoff
- โ Production-ready performance (25+ req/s sustained)
- โ Enterprise-scale validation (400 concurrent operations)
- โ 10x Performance Improvement: Tests complete in 30-35s vs 5-10 minutes
๐งช Test Coverage
ACME Love maintains comprehensive test coverage to ensure reliability and quality:
Test Statistics
- โ 60 Tests across 18 test suites
- โ 100% Passing test rate
- ๐ Core Components Coverage:
csr.ts: 94.11% (cryptographic operations)nonce-manager.ts: 68.03% (pooling & concurrent access)acme-directory.ts: 83.33% (directory operations)acme-client-core.ts: 93.75% (core client functionality)
Note: Stress tests are excluded from the default npm test command to keep CI/CD pipelines fast. They should be run manually or in dedicated test environments.
๐ Test Account Management
ACME Love includes a sophisticated test account management system to avoid Let's Encrypt rate limits during development and testing:
# Prepare accounts for all stress tests (run once)
npm run accounts prepare-stress
# List all test accounts
npm run accounts list
# Create specific account
npm run accounts create my-test-account
# Clean up old accounts (older than 24 hours)
npm run accounts cleanup 24Benefits:
- โ Avoid Let's Encrypt's 50 registrations per IP per 3 hours limit
- โ Faster test execution (reuse existing accounts)
- โ Isolated accounts per test type
- โ Automatic git ignore protection
๐ Detailed account management guide: TEST-ACCOUNT-MANAGEMENT.md
๐ License
ISC License - see LICENSE file for details.
๐ค Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests.
Made with โค๏ธ for the Node.js community
