|
|
|
@ -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,64 @@ 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',
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
SmartAcme uses the `ICertManager` interface for certificate storage. Two built-in implementations are available:
|
|
|
|
|
|
|
|
|
|
- **MemoryCertManager**
|
|
|
|
|
- In-memory storage, suitable for testing or ephemeral use.
|
|
|
|
|
- Import example:
|
|
|
|
|
```typescript
|
|
|
|
|
import { MemoryCertManager } from '@push.rocks/smartacme';
|
|
|
|
|
const certManager = new MemoryCertManager();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **MongoCertManager**
|
|
|
|
|
- Persistent storage in MongoDB (collection: `SmartacmeCert`).
|
|
|
|
|
- Import example:
|
|
|
|
|
```typescript
|
|
|
|
|
import { MongoCertManager } from '@push.rocks/smartacme';
|
|
|
|
|
const certManager = new MongoCertManager({
|
|
|
|
|
mongoDbUrl: 'mongodb://yourmongoURL',
|
|
|
|
|
mongoDbName: 'yourDbName',
|
|
|
|
|
mongoDbPass: 'yourDbPassword',
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### Custom Certificate Managers
|
|
|
|
|
|
|
|
|
|
To implement a custom certificate manager, implement the `ICertManager` interface and pass it to `SmartAcme`:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import type { ICertManager, Cert as SmartacmeCert } from '@push.rocks/smartacme';
|
|
|
|
|
import { SmartAcme } from '@push.rocks/smartacme';
|
|
|
|
|
|
|
|
|
|
class MyCustomCertManager implements ICertManager {
|
|
|
|
|
async init(): Promise<void> { /* setup storage */ }
|
|
|
|
|
async get(domainName: string): Promise<SmartacmeCert | null> { /* lookup cert */ }
|
|
|
|
|
async put(cert: SmartacmeCert): Promise<SmartacmeCert> { /* store cert */ }
|
|
|
|
|
async delete(domainName: string): Promise<void> { /* remove cert */ }
|
|
|
|
|
async close?(): Promise<void> { /* optional cleanup */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use your custom manager:
|
|
|
|
|
const customManager = new MyCustomCertManager();
|
|
|
|
|
const smartAcme = new SmartAcme({
|
|
|
|
|
accountEmail: 'youremail@example.com',
|
|
|
|
|
certManager: customManager,
|
|
|
|
|
environment: 'integration',
|
|
|
|
|
challengeHandlers: [], // add your handlers
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Environmental Considerations
|
|
|
|
@ -110,23 +170,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 +202,8 @@ async function main() {
|
|
|
|
|
await smartAcmeInstance.stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch(console.error);
|
|
|
|
|
```
|
|
|
|
|
main().catch(console.error);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Built-in Challenge Handlers
|
|
|
|
|
|
|
|
|
@ -222,7 +286,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 +307,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 +322,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`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|