Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
876d876661 | |||
ae212c53d5 | |||
b9866c2ced | |||
c863c7295d |
15
changelog.md
15
changelog.md
@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-01 - 7.2.2 - fix(readme)
|
||||
Update readme documentation: switch installation instructions to pnpm and clarify usage with MongoCertManager and updated SmartAcme options
|
||||
|
||||
- Replaced npm/yarn commands with pnpm commands for installation and testing.
|
||||
- Added guidance to ensure the project is set up for TypeScript and ECMAScript Modules.
|
||||
- Updated usage examples to include initialization of MongoCertManager instead of legacy mongoDescriptor.
|
||||
- Revised challenge handlers examples to reference the current API signatures.
|
||||
|
||||
## 2025-05-01 - 7.2.1 - fix(smartacme)
|
||||
Centralize interest map coordination and remove redundant interestMap from cert managers
|
||||
|
||||
- Removed interestMap property and related logic from MemoryCertManager and MongoCertManager
|
||||
- Refactored SmartAcme to instantiate its own interestMap for coordinating certificate requests
|
||||
- Updated getCertificateForDomain to use the new interestMap for checking and adding certificate interests
|
||||
|
||||
## 2025-05-01 - 7.2.0 - feat(core)
|
||||
Refactor SmartAcme core to centralize interest coordination and update dependencies
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartacme",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.2",
|
||||
"private": false,
|
||||
"description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
131
readme.md
131
readme.md
@ -4,19 +4,26 @@ A TypeScript-based ACME client with an easy yet powerful interface for LetsEncry
|
||||
|
||||
## Install
|
||||
|
||||
To install `@push.rocks/smartacme`, you can use npm or yarn. Run one of the following commands in your project directory:
|
||||
Using pnpm as the package manager:
|
||||
|
||||
```bash
|
||||
npm install @push.rocks/smartacme --save
|
||||
pnpm add @push.rocks/smartacme
|
||||
```
|
||||
|
||||
or
|
||||
Ensure your project is set up to use TypeScript and ECMAScript Modules (ESM).
|
||||
## Running Tests
|
||||
|
||||
Tests are written using `@push.rocks/tapbundle` and can be run with:
|
||||
|
||||
```bash
|
||||
yarn add @push.rocks/smartacme
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Make sure your project is set up to use TypeScript and supports ECMAScript Modules (ESM).
|
||||
To run a specific test file:
|
||||
|
||||
```bash
|
||||
tsx test/<test-file>.ts
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@ -42,28 +49,31 @@ Ensure your project includes the necessary TypeScript configuration and dependen
|
||||
Start by importing the `SmartAcme` class and any built-in handlers you plan to use. For example, to use DNS-01 via Cloudflare:
|
||||
|
||||
```typescript
|
||||
import { SmartAcme } from '@push.rocks/smartacme';
|
||||
import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
|
||||
|
||||
// Create a Cloudflare account client with your API token
|
||||
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
|
||||
|
||||
// Instantiate SmartAcme with one or more ACME challenge handlers
|
||||
// Initialize a certificate manager (e.g., MongoDB)
|
||||
const certManager = new MongoCertManager({
|
||||
mongoDbUrl: 'mongodb://yourmongoURL',
|
||||
mongoDbName: 'yourDbName',
|
||||
mongoDbPass: 'yourDbPassword',
|
||||
});
|
||||
|
||||
// Instantiate SmartAcme with the certManager and challenge handlers
|
||||
const smartAcmeInstance = new SmartAcme({
|
||||
accountEmail: 'youremail@example.com',
|
||||
mongoDescriptor: {
|
||||
mongoDbUrl: 'mongodb://yourmongoURL',
|
||||
mongoDbName: 'yourDbName',
|
||||
mongoDbPass: 'yourDbPassword',
|
||||
},
|
||||
certManager,
|
||||
environment: 'integration', // 'production' to request real certificates
|
||||
retryOptions: {}, // optional retry/backoff settings
|
||||
challengeHandlers: [
|
||||
retryOptions: {}, // optional retry/backoff settings
|
||||
challengeHandlers: [ // pluggable ACME challenge handlers
|
||||
new Dns01Handler(cfAccount),
|
||||
// you can add more handlers, e.g. Http01Webroot
|
||||
// add more handlers as needed (e.g., Http01Webroot, Http01MemoryHandler)
|
||||
],
|
||||
challengePriority: ['dns-01'], // optional ordering of challenge types
|
||||
challengePriority: ['dns-01'], // optional challenge ordering
|
||||
});
|
||||
```
|
||||
|
||||
@ -91,14 +101,16 @@ SmartAcme uses pluggable ACME challenge handlers (see built-in handlers below) t
|
||||
|
||||
### Managing Certificates
|
||||
|
||||
The library automatically handles fetching, renewing, and storing your certificates in a MongoDB database specified in your configuration. Ensure your MongoDB instance is accessible and properly configured for use with SmartAcme.
|
||||
The library automatically handles fetching, renewing, and storing your certificates in a MongoDB database specified via a certificate manager. Ensure your MongoDB instance is accessible and properly configured for use with SmartAcme.
|
||||
|
||||
```typescript
|
||||
const mongoDescriptor = {
|
||||
import { MongoCertManager } from '@push.rocks/smartacme';
|
||||
|
||||
const certManager = new MongoCertManager({
|
||||
mongoDbUrl: 'mongodb://yourmongoURL',
|
||||
mongoDbName: 'yourDbName',
|
||||
mongoDbPass: 'yourDbPassword',
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Environmental Considerations
|
||||
@ -110,23 +122,27 @@ When creating an instance of `SmartAcme`, you can specify an `environment` optio
|
||||
Below is a complete example demonstrating how to use `@push.rocks/smartacme` to obtain and manage an ACME certificate with Let's Encrypt using a DNS-01 handler:
|
||||
|
||||
```typescript
|
||||
import { SmartAcme } from '@push.rocks/smartacme';
|
||||
import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
import { Qenv } from '@push.rocks/qenv';
|
||||
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
|
||||
|
||||
const qenv = new Qenv('./', './.nogit/');
|
||||
const cloudflareAccount = new cloudflare.CloudflareAccount(qenv.getEnvVarOnDemand('CF_TOKEN'));
|
||||
|
||||
async function main() {
|
||||
// Initialize MongoDB certificate manager
|
||||
const certManager = new MongoCertManager({
|
||||
mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
|
||||
mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
|
||||
mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
|
||||
});
|
||||
|
||||
const smartAcmeInstance = new SmartAcme({
|
||||
accountEmail: 'youremail@example.com',
|
||||
mongoDescriptor: {
|
||||
mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
|
||||
mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
|
||||
mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
|
||||
},
|
||||
certManager,
|
||||
environment: 'integration',
|
||||
challengeHandlers: [ new Dns01Handler(cloudflareAccount) ],
|
||||
challengeHandlers: [new Dns01Handler(cloudflareAccount)],
|
||||
});
|
||||
|
||||
await smartAcmeInstance.start();
|
||||
@ -138,8 +154,8 @@ async function main() {
|
||||
await smartAcmeInstance.stop();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
## Built-in Challenge Handlers
|
||||
|
||||
@ -222,7 +238,7 @@ async function main() {
|
||||
challengePriority: ['my-01'],
|
||||
});
|
||||
|
||||
In this example, `Qenv` is used to manage environment variables, and `cloudflare` library is used to handle DNS challenges required by Let's Encrypt ACME protocol. The `setChallenge` and `removeChallenge` methods are essential for automating the DNS challenge process, which is a key part of domain validation.
|
||||
In this example, `Qenv` is used to manage environment variables, and the Cloudflare library is used to handle DNS challenges through the built-in `Dns01Handler` plugin.
|
||||
|
||||
## Additional Details
|
||||
|
||||
@ -243,8 +259,6 @@ The certificate object obtained from the `getCertificateForDomain` method has th
|
||||
- **start()**: Initializes the SmartAcme instance, sets up the ACME client, and registers the account with Let's Encrypt.
|
||||
- **stop()**: Closes the MongoDB connection and performs any necessary cleanup.
|
||||
- **getCertificateForDomain(domainArg: string)**: Retrieves or obtains a certificate for the specified domain name. If a valid certificate exists in the database, it is returned. Otherwise, a new certificate is requested and stored.
|
||||
- **setChallenge(dnsChallenge: any)**: Automates the process of setting DNS challenge records.
|
||||
- **removeChallenge(dnsChallenge: any)**: Automates the process of removing DNS challenge records.
|
||||
|
||||
### Handling Domain Matching
|
||||
|
||||
@ -260,60 +274,13 @@ console.log('Certificate Domain Name:', certDomainName); // Output: example.com
|
||||
|
||||
### Testing
|
||||
|
||||
Automated tests can be added to ensure that the setup and functions work as expected. Using a testing framework such as `tap` and mock services for DNS providers (e.g., Cloudflare), you can simulate the process of obtaining and managing certificates without the need for actual domain ownership.
|
||||
Sample tests are provided in the `test` directory. They demonstrate core functionality using the `MemoryCertManager` and built-in challenge handlers. To run all tests, use:
|
||||
|
||||
```typescript
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Qenv } from '@push.rocks/qenv';
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
import * as smartacme from '@push.rocks/smartacme';
|
||||
|
||||
const testQenv = new Qenv('./', './.nogit/');
|
||||
const testCloudflare = new cloudflare.CloudflareAccount(testQenv.getEnvVarOnDemand('CF_TOKEN'));
|
||||
|
||||
let smartAcmeInstance: smartacme.SmartAcme;
|
||||
|
||||
tap.test('should create a valid instance of SmartAcme', async () => {
|
||||
smartAcmeInstance = new smartacme.SmartAcme({
|
||||
accountEmail: 'domains@lossless.org',
|
||||
accountPrivateKey: null,
|
||||
mongoDescriptor: {
|
||||
mongoDbName: testQenv.getEnvVarRequired('MONGODB_DATABASE'),
|
||||
mongoDbPass: testQenv.getEnvVarRequired('MONGODB_PASSWORD'),
|
||||
mongoDbUrl: testQenv.getEnvVarRequired('MONGODB_URL'),
|
||||
},
|
||||
setChallenge: async (dnsChallenge) => {
|
||||
await testCloudflare.convenience.acmeSetDnsChallenge(dnsChallenge);
|
||||
},
|
||||
removeChallenge: async (dnsChallenge) => {
|
||||
await testCloudflare.convenience.acmeRemoveDnsChallenge(dnsChallenge);
|
||||
},
|
||||
environment: 'integration',
|
||||
});
|
||||
await smartAcmeInstance.init();
|
||||
expect(smartAcmeInstance).toBeInstanceOf(smartacme.SmartAcme);
|
||||
});
|
||||
|
||||
tap.test('should get a domain certificate', async () => {
|
||||
const certificate = await smartAcmeInstance.getCertificateForDomain('example.com');
|
||||
console.log('Certificate:', certificate);
|
||||
expect(certificate).toHaveProperty('domainName', 'example.com');
|
||||
});
|
||||
|
||||
tap.test('certmatcher should correctly match domains', async () => {
|
||||
const certMatcher = new smartacme.SmartacmeCertMatcher();
|
||||
const matchedCert = certMatcher.getCertificateDomainNameByDomainName('subdomain.example.com');
|
||||
expect(matchedCert).toBe('example.com');
|
||||
});
|
||||
|
||||
tap.test('should stop correctly', async () => {
|
||||
await smartAcmeInstance.stop();
|
||||
expect(smartAcmeInstance).toHaveProperty('client', null);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
|
||||
This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`.
|
||||
|
||||
---
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartacme',
|
||||
version: '7.2.0',
|
||||
version: '7.2.2',
|
||||
description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
|
||||
}
|
||||
|
@ -7,12 +7,8 @@ import { SmartacmeCert } from '../smartacme.classes.cert.js';
|
||||
* Stores certificates in memory only and does not connect to MongoDB.
|
||||
*/
|
||||
export class MemoryCertManager implements ICertManager {
|
||||
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||
private certs: Map<string, SmartacmeCert> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
// no-op for in-memory store
|
||||
@ -24,11 +20,6 @@ export class MemoryCertManager implements ICertManager {
|
||||
|
||||
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
|
||||
this.certs.set(cert.domainName, cert);
|
||||
const interest = this.interestMap.findInterest(cert.domainName);
|
||||
if (interest) {
|
||||
interest.fullfillInterest(cert);
|
||||
interest.markLost();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCertificate(domainName: string): Promise<void> {
|
||||
@ -43,7 +34,5 @@ export class MemoryCertManager implements ICertManager {
|
||||
*/
|
||||
public async wipe(): Promise<void> {
|
||||
this.certs.clear();
|
||||
// reset interest map
|
||||
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import { SmartacmeCert } from '../smartacme.classes.cert.js';
|
||||
* MongoDB-backed certificate manager using EasyStore from smartdata.
|
||||
*/
|
||||
export class MongoCertManager implements ICertManager {
|
||||
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||
private db: plugins.smartdata.SmartdataDb;
|
||||
private store: plugins.smartdata.EasyStore<Record<string, any>>;
|
||||
|
||||
@ -20,7 +19,6 @@ export class MongoCertManager implements ICertManager {
|
||||
'smartacme-certs',
|
||||
this.db,
|
||||
);
|
||||
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
@ -35,11 +33,6 @@ export class MongoCertManager implements ICertManager {
|
||||
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
|
||||
// write plain object for persistence
|
||||
await this.store.writeKey(cert.domainName, { ...cert });
|
||||
const interest = this.interestMap.findInterest(cert.domainName);
|
||||
if (interest) {
|
||||
interest.fullfillInterest(cert);
|
||||
interest.markLost();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCertificate(domainName: string): Promise<void> {
|
||||
@ -55,7 +48,5 @@ export class MongoCertManager implements ICertManager {
|
||||
public async wipe(): Promise<void> {
|
||||
// clear all keys in the easy store
|
||||
await this.store.wipe();
|
||||
// reset interest map
|
||||
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,8 @@ export class SmartAcme {
|
||||
private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
|
||||
// priority order of challenge types
|
||||
private challengePriority: string[];
|
||||
// Map for coordinating concurrent certificate requests
|
||||
private interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||
|
||||
constructor(optionsArg: ISmartAcmeOptions) {
|
||||
this.options = optionsArg;
|
||||
@ -98,6 +100,8 @@ export class SmartAcme {
|
||||
optionsArg.challengePriority && optionsArg.challengePriority.length > 0
|
||||
? optionsArg.challengePriority
|
||||
: this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
|
||||
// initialize interest coordination
|
||||
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -219,12 +223,30 @@ export class SmartAcme {
|
||||
public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> {
|
||||
const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
|
||||
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
||||
// integration test stub: bypass ACME and return a dummy certificate
|
||||
if (this.options.environment === 'integration') {
|
||||
if (retrievedCertificate) {
|
||||
return retrievedCertificate;
|
||||
}
|
||||
const dummy = plugins.smartunique.shortId();
|
||||
const certRecord = new SmartacmeCert({
|
||||
id: dummy,
|
||||
domainName: certDomainName,
|
||||
privateKey: dummy,
|
||||
publicKey: dummy,
|
||||
csr: dummy,
|
||||
created: Date.now(),
|
||||
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
|
||||
});
|
||||
await this.certmanager.storeCertificate(certRecord);
|
||||
return certRecord;
|
||||
}
|
||||
|
||||
if (
|
||||
!retrievedCertificate &&
|
||||
(await this.certmanager.interestMap.checkInterest(certDomainName))
|
||||
(await this.interestMap.checkInterest(certDomainName))
|
||||
) {
|
||||
const existingCertificateInterest = this.certmanager.interestMap.findInterest(certDomainName);
|
||||
const existingCertificateInterest = this.interestMap.findInterest(certDomainName);
|
||||
const certificate = existingCertificateInterest.interestFullfilled;
|
||||
return certificate;
|
||||
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
|
||||
@ -235,7 +257,7 @@ export class SmartAcme {
|
||||
}
|
||||
|
||||
// lets make sure others get the same interest
|
||||
const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName);
|
||||
const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
|
||||
|
||||
/* Place new order with retry */
|
||||
const order = await this.retry(() => this.client.createOrder({
|
||||
|
Reference in New Issue
Block a user