@push.rocks/smartacme 8.0.0 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/acme/acme.classes.account.d.ts +20 -0
- package/dist_ts/acme/acme.classes.account.js +37 -0
- package/dist_ts/acme/acme.classes.challenge.d.ts +22 -0
- package/dist_ts/acme/acme.classes.challenge.js +36 -0
- package/dist_ts/acme/acme.classes.client.d.ts +72 -0
- package/dist_ts/acme/acme.classes.client.js +77 -0
- package/dist_ts/acme/acme.classes.crypto.d.ts +50 -0
- package/dist_ts/acme/acme.classes.crypto.js +180 -0
- package/dist_ts/acme/acme.classes.directory.d.ts +13 -0
- package/dist_ts/acme/acme.classes.directory.js +14 -0
- package/dist_ts/acme/acme.classes.error.d.ts +44 -0
- package/dist_ts/acme/acme.classes.error.js +41 -0
- package/dist_ts/acme/acme.classes.http-client.d.ts +42 -0
- package/dist_ts/acme/acme.classes.http-client.js +211 -0
- package/dist_ts/acme/acme.classes.order.d.ts +40 -0
- package/dist_ts/acme/acme.classes.order.js +100 -0
- package/dist_ts/acme/acme.interfaces.d.ts +64 -0
- package/dist_ts/acme/acme.interfaces.js +5 -0
- package/dist_ts/acme/index.d.ts +5 -0
- package/dist_ts/acme/index.js +5 -0
- package/dist_ts/certmanagers/mongo.js +1 -2
- package/dist_ts/plugins.d.ts +2 -7
- package/dist_ts/plugins.js +5 -11
- package/dist_ts/smartacme.classes.smartacme.d.ts +3 -1
- package/dist_ts/smartacme.classes.smartacme.js +48 -13
- package/npmextra.json +12 -6
- package/package.json +23 -24
- package/readme.hints.md +27 -3
- package/readme.md +245 -267
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/acme.classes.account.ts +45 -0
- package/ts/acme/acme.classes.challenge.ts +45 -0
- package/ts/acme/acme.classes.client.ts +106 -0
- package/ts/acme/acme.classes.crypto.ts +220 -0
- package/ts/acme/acme.classes.directory.ts +13 -0
- package/ts/acme/acme.classes.error.ts +55 -0
- package/ts/acme/acme.classes.http-client.ts +249 -0
- package/ts/acme/acme.classes.order.ts +125 -0
- package/ts/acme/acme.interfaces.ts +74 -0
- package/ts/acme/index.ts +16 -0
- package/ts/certmanagers/mongo.ts +0 -1
- package/ts/plugins.ts +3 -14
- package/ts/smartacme.classes.smartacme.ts +49 -16
- package/dist_ts/certmanagers.d.ts +0 -42
- package/dist_ts/certmanagers.js +0 -86
- package/dist_ts/smartacme.classes.certmanager.d.ts +0 -43
- package/dist_ts/smartacme.classes.certmanager.js +0 -92
- package/dist_ts/smartacme.plugins.d.ts +0 -21
- package/dist_ts/smartacme.plugins.js +0 -28
package/readme.md
CHANGED
|
@@ -1,361 +1,339 @@
|
|
|
1
1
|
# @push.rocks/smartacme
|
|
2
2
|
|
|
3
|
-
A TypeScript-based ACME client
|
|
3
|
+
A TypeScript-based ACME client for Let's Encrypt certificate management with a focus on simplicity and power. 🔒
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Issue Reporting and Security
|
|
6
|
+
|
|
7
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://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/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
pnpm add @push.rocks/smartacme
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
Ensure your project
|
|
14
|
-
## Running Tests
|
|
15
|
+
Ensure your project uses TypeScript and ECMAScript Modules (ESM).
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
## Usage
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
`@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), and structured error handling with smart retry logic.
|
|
20
|
+
|
|
21
|
+
### 🚀 Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { SmartAcme, certmanagers, handlers } from '@push.rocks/smartacme';
|
|
25
|
+
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
26
|
+
|
|
27
|
+
// 1. Set up a certificate manager (MongoDB or in-memory)
|
|
28
|
+
const certManager = new certmanagers.MongoCertManager({
|
|
29
|
+
mongoDbUrl: 'mongodb://localhost:27017',
|
|
30
|
+
mongoDbName: 'myapp',
|
|
31
|
+
mongoDbPass: 'secret',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 2. Set up challenge handlers
|
|
35
|
+
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_API_TOKEN');
|
|
36
|
+
const dnsHandler = new handlers.Dns01Handler(cfAccount);
|
|
37
|
+
|
|
38
|
+
// 3. Create and start SmartAcme
|
|
39
|
+
const smartAcme = new SmartAcme({
|
|
40
|
+
accountEmail: 'admin@example.com',
|
|
41
|
+
certManager,
|
|
42
|
+
environment: 'production', // or 'integration' for staging
|
|
43
|
+
challengeHandlers: [dnsHandler],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await smartAcme.start();
|
|
47
|
+
|
|
48
|
+
// 4. Get a certificate
|
|
49
|
+
const cert = await smartAcme.getCertificateForDomain('example.com');
|
|
50
|
+
console.log(cert.publicKey); // PEM certificate chain
|
|
51
|
+
console.log(cert.privateKey); // PEM private key
|
|
52
|
+
|
|
53
|
+
// 5. Clean up
|
|
54
|
+
await smartAcme.stop();
|
|
20
55
|
```
|
|
21
56
|
|
|
22
|
-
|
|
57
|
+
### ⚙️ SmartAcme Options
|
|
23
58
|
|
|
24
|
-
```
|
|
25
|
-
|
|
59
|
+
```typescript
|
|
60
|
+
interface ISmartAcmeOptions {
|
|
61
|
+
accountEmail: string; // ACME account email
|
|
62
|
+
accountPrivateKey?: string; // Optional account key (auto-generated if omitted)
|
|
63
|
+
certManager: ICertManager; // Certificate storage backend
|
|
64
|
+
environment: 'production' | 'integration'; // Let's Encrypt environment
|
|
65
|
+
challengeHandlers: IChallengeHandler[]; // At least one handler required
|
|
66
|
+
challengePriority?: string[]; // e.g. ['dns-01', 'http-01']
|
|
67
|
+
retryOptions?: { // Optional retry/backoff config
|
|
68
|
+
retries?: number; // Default: 10
|
|
69
|
+
factor?: number; // Default: 4
|
|
70
|
+
minTimeoutMs?: number; // Default: 1000
|
|
71
|
+
maxTimeoutMs?: number; // Default: 60000
|
|
72
|
+
};
|
|
73
|
+
}
|
|
26
74
|
```
|
|
27
75
|
|
|
28
|
-
|
|
76
|
+
### 📜 Getting Certificates
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Standard certificate for a single domain
|
|
80
|
+
const cert = await smartAcme.getCertificateForDomain('example.com');
|
|
29
81
|
|
|
30
|
-
|
|
82
|
+
// Include wildcard coverage (requires DNS-01 handler)
|
|
83
|
+
// Issues a single cert covering example.com AND *.example.com
|
|
84
|
+
const certWithWildcard = await smartAcme.getCertificateForDomain('example.com', {
|
|
85
|
+
includeWildcard: true,
|
|
86
|
+
});
|
|
31
87
|
|
|
32
|
-
|
|
88
|
+
// Request wildcard only
|
|
89
|
+
const wildcardCert = await smartAcme.getCertificateForDomain('*.example.com');
|
|
90
|
+
```
|
|
33
91
|
|
|
34
|
-
|
|
35
|
-
2. [Creating a SmartAcme Instance](#creating-a-smartacme-instance)
|
|
36
|
-
3. [Initializing SmartAcme](#initializing-smartacme)
|
|
37
|
-
4. [Obtaining a Certificate for a Domain](#obtaining-a-certificate-for-a-domain)
|
|
38
|
-
5. [Automating DNS Challenges](#automating-dns-challenges)
|
|
39
|
-
6. [Managing Certificates](#managing-certificates)
|
|
40
|
-
7. [Environmental Considerations](#environmental-considerations)
|
|
41
|
-
8. [Complete Example](#complete-example)
|
|
92
|
+
Certificates are automatically cached and reused when still valid. Renewal happens automatically when a certificate is within 10 days of expiration.
|
|
42
93
|
|
|
43
|
-
###
|
|
94
|
+
### 📦 Certificate Object
|
|
44
95
|
|
|
45
|
-
|
|
96
|
+
The returned `SmartacmeCert` (also exported as `Cert`) object has these properties:
|
|
46
97
|
|
|
47
|
-
|
|
98
|
+
| Property | Type | Description |
|
|
99
|
+
|-------------|----------|--------------------------------------|
|
|
100
|
+
| `id` | `string` | Unique certificate identifier |
|
|
101
|
+
| `domainName`| `string` | Domain the cert is issued for |
|
|
102
|
+
| `publicKey` | `string` | PEM-encoded certificate chain |
|
|
103
|
+
| `privateKey`| `string` | PEM-encoded private key |
|
|
104
|
+
| `csr` | `string` | Certificate Signing Request |
|
|
105
|
+
| `created` | `number` | Timestamp of creation |
|
|
106
|
+
| `validUntil`| `number` | Timestamp of expiration |
|
|
48
107
|
|
|
49
|
-
|
|
108
|
+
Useful methods:
|
|
50
109
|
|
|
51
110
|
```typescript
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
111
|
+
cert.isStillValid(); // true if not expired
|
|
112
|
+
cert.shouldBeRenewed(); // true if expires within 10 days
|
|
113
|
+
```
|
|
55
114
|
|
|
56
|
-
|
|
57
|
-
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
|
|
115
|
+
## Certificate Managers
|
|
58
116
|
|
|
59
|
-
|
|
60
|
-
const certManager = new MongoCertManager({
|
|
61
|
-
mongoDbUrl: 'mongodb://yourmongoURL',
|
|
62
|
-
mongoDbName: 'yourDbName',
|
|
63
|
-
mongoDbPass: 'yourDbPassword',
|
|
64
|
-
});
|
|
117
|
+
SmartAcme uses the `ICertManager` interface for pluggable certificate storage.
|
|
65
118
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
119
|
+
### 🗄️ MongoCertManager
|
|
120
|
+
|
|
121
|
+
Persistent storage backed by MongoDB using `@push.rocks/smartdata`:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { certmanagers } from '@push.rocks/smartacme';
|
|
125
|
+
|
|
126
|
+
const certManager = new certmanagers.MongoCertManager({
|
|
127
|
+
mongoDbUrl: 'mongodb://localhost:27017',
|
|
128
|
+
mongoDbName: 'myapp',
|
|
129
|
+
mongoDbPass: 'secret',
|
|
77
130
|
});
|
|
78
131
|
```
|
|
79
132
|
|
|
80
|
-
###
|
|
133
|
+
### 🧪 MemoryCertManager
|
|
81
134
|
|
|
82
|
-
|
|
135
|
+
In-memory storage, ideal for testing or ephemeral workloads:
|
|
83
136
|
|
|
84
137
|
```typescript
|
|
85
|
-
|
|
138
|
+
import { certmanagers } from '@push.rocks/smartacme';
|
|
139
|
+
|
|
140
|
+
const certManager = new certmanagers.MemoryCertManager();
|
|
86
141
|
```
|
|
87
142
|
|
|
88
|
-
###
|
|
143
|
+
### 🔧 Custom Certificate Manager
|
|
89
144
|
|
|
90
|
-
|
|
145
|
+
Implement the `ICertManager` interface for your own storage backend:
|
|
91
146
|
|
|
92
147
|
```typescript
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
148
|
+
import type { ICertManager, Cert } from '@push.rocks/smartacme';
|
|
149
|
+
|
|
150
|
+
class RedisCertManager implements ICertManager {
|
|
151
|
+
async init(): Promise<void> { /* connect */ }
|
|
152
|
+
async retrieveCertificate(domainName: string): Promise<Cert | null> { /* lookup */ }
|
|
153
|
+
async storeCertificate(cert: Cert): Promise<void> { /* save */ }
|
|
154
|
+
async deleteCertificate(domainName: string): Promise<void> { /* remove */ }
|
|
155
|
+
async close(): Promise<void> { /* disconnect */ }
|
|
156
|
+
async wipe(): Promise<void> { /* clear all */ }
|
|
157
|
+
}
|
|
96
158
|
```
|
|
97
159
|
|
|
98
|
-
|
|
160
|
+
## Challenge Handlers
|
|
161
|
+
|
|
162
|
+
SmartAcme ships with three built-in ACME challenge handlers. All implement `IChallengeHandler<T>`.
|
|
99
163
|
|
|
100
|
-
|
|
164
|
+
### 🌐 Dns01Handler
|
|
101
165
|
|
|
102
|
-
|
|
166
|
+
Uses Cloudflare (or any `IConvenientDnsProvider`) to set and remove DNS TXT records for `dns-01` challenges:
|
|
103
167
|
|
|
104
|
-
|
|
168
|
+
```typescript
|
|
169
|
+
import { handlers } from '@push.rocks/smartacme';
|
|
170
|
+
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
171
|
+
|
|
172
|
+
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
|
|
173
|
+
const dnsHandler = new handlers.Dns01Handler(cfAccount);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
DNS-01 is **required** for wildcard certificates and works regardless of server accessibility.
|
|
177
|
+
|
|
178
|
+
### 📁 Http01Webroot
|
|
179
|
+
|
|
180
|
+
Writes challenge response files to a filesystem webroot for `http-01` validation:
|
|
105
181
|
|
|
106
182
|
```typescript
|
|
107
|
-
import {
|
|
183
|
+
import { handlers } from '@push.rocks/smartacme';
|
|
108
184
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
mongoDbName: 'yourDbName',
|
|
112
|
-
mongoDbPass: 'yourDbPassword',
|
|
185
|
+
const httpHandler = new handlers.Http01Webroot({
|
|
186
|
+
webroot: '/var/www/html',
|
|
113
187
|
});
|
|
114
188
|
```
|
|
115
189
|
|
|
116
|
-
|
|
190
|
+
The handler writes to `<webroot>/.well-known/acme-challenge/<token>` and cleans up after validation.
|
|
191
|
+
|
|
192
|
+
### 🧠 Http01MemoryHandler
|
|
193
|
+
|
|
194
|
+
In-memory HTTP-01 handler — stores challenge tokens in memory and serves them via `handleRequest()`:
|
|
117
195
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
import { MemoryCertManager } from '@push.rocks/smartacme';
|
|
123
|
-
const certManager = new MemoryCertManager();
|
|
124
|
-
```
|
|
196
|
+
```typescript
|
|
197
|
+
import { handlers } from '@push.rocks/smartacme';
|
|
198
|
+
|
|
199
|
+
const memHandler = new handlers.Http01MemoryHandler();
|
|
125
200
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
import { MongoCertManager } from '@push.rocks/smartacme';
|
|
131
|
-
const certManager = new MongoCertManager({
|
|
132
|
-
mongoDbUrl: 'mongodb://yourmongoURL',
|
|
133
|
-
mongoDbName: 'yourDbName',
|
|
134
|
-
mongoDbPass: 'yourDbPassword',
|
|
135
|
-
});
|
|
136
|
-
```
|
|
201
|
+
// Integrate with any HTTP server (Express, Koa, raw http, etc.)
|
|
202
|
+
app.use((req, res, next) => memHandler.handleRequest(req, res, next));
|
|
203
|
+
```
|
|
137
204
|
|
|
138
|
-
|
|
205
|
+
Perfect for serverless or container environments where filesystem access is limited.
|
|
139
206
|
|
|
140
|
-
|
|
207
|
+
### 🔧 Custom Challenge Handler
|
|
208
|
+
|
|
209
|
+
Implement `IChallengeHandler<T>` for custom challenge types:
|
|
141
210
|
|
|
142
211
|
```typescript
|
|
143
|
-
import type {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
async put(cert: SmartacmeCert): Promise<SmartacmeCert> { /* store cert */ }
|
|
150
|
-
async delete(domainName: string): Promise<void> { /* remove cert */ }
|
|
151
|
-
async close?(): Promise<void> { /* optional cleanup */ }
|
|
212
|
+
import type { handlers } from '@push.rocks/smartacme';
|
|
213
|
+
|
|
214
|
+
interface MyChallenge {
|
|
215
|
+
type: string;
|
|
216
|
+
token: string;
|
|
217
|
+
keyAuthorization: string;
|
|
152
218
|
}
|
|
153
219
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
challengeHandlers: [], // add your handlers
|
|
161
|
-
});
|
|
220
|
+
class MyHandler implements handlers.IChallengeHandler<MyChallenge> {
|
|
221
|
+
getSupportedTypes(): string[] { return ['http-01']; }
|
|
222
|
+
async prepare(ch: MyChallenge): Promise<void> { /* set up challenge response */ }
|
|
223
|
+
async cleanup(ch: MyChallenge): Promise<void> { /* tear down */ }
|
|
224
|
+
async checkWetherDomainIsSupported(domain: string): Promise<boolean> { return true; }
|
|
225
|
+
}
|
|
162
226
|
```
|
|
163
227
|
|
|
164
|
-
|
|
228
|
+
## Error Handling
|
|
165
229
|
|
|
166
|
-
|
|
230
|
+
SmartAcme provides structured ACME error handling via the `AcmeError` class, which carries full RFC 8555 error information:
|
|
167
231
|
|
|
168
|
-
|
|
232
|
+
```typescript
|
|
233
|
+
import { AcmeError } from '@push.rocks/smartacme/ts/acme/acme.classes.error.js';
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const cert = await smartAcme.getCertificateForDomain('example.com');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (err instanceof AcmeError) {
|
|
239
|
+
console.log(err.status); // HTTP status code (e.g. 429)
|
|
240
|
+
console.log(err.type); // ACME error URN (e.g. 'urn:ietf:params:acme:error:rateLimited')
|
|
241
|
+
console.log(err.detail); // Human-readable message
|
|
242
|
+
console.log(err.subproblems); // Per-identifier sub-errors (RFC 8555 §6.7.1)
|
|
243
|
+
console.log(err.retryAfter); // Retry-After value in seconds
|
|
244
|
+
console.log(err.isRateLimited); // true for 429 or rateLimited type
|
|
245
|
+
console.log(err.isRetryable); // true for 429, 503, 5xx, badNonce; false for 403/404/409
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
169
249
|
|
|
170
|
-
|
|
250
|
+
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.
|
|
171
251
|
|
|
172
|
-
|
|
173
|
-
import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
|
|
174
|
-
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
175
|
-
import { Qenv } from '@push.rocks/qenv';
|
|
176
|
-
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
|
|
177
|
-
|
|
178
|
-
const qenv = new Qenv('./', './.nogit/');
|
|
179
|
-
const cloudflareAccount = new cloudflare.CloudflareAccount(qenv.getEnvVarOnDemand('CF_TOKEN'));
|
|
180
|
-
|
|
181
|
-
async function main() {
|
|
182
|
-
// Initialize MongoDB certificate manager
|
|
183
|
-
const certManager = new MongoCertManager({
|
|
184
|
-
mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
|
|
185
|
-
mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
|
|
186
|
-
mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
|
|
187
|
-
});
|
|
252
|
+
## Domain Matching
|
|
188
253
|
|
|
189
|
-
|
|
190
|
-
accountEmail: 'youremail@example.com',
|
|
191
|
-
certManager,
|
|
192
|
-
environment: 'integration',
|
|
193
|
-
challengeHandlers: [new Dns01Handler(cloudflareAccount)],
|
|
194
|
-
});
|
|
254
|
+
SmartAcme automatically maps subdomains to their base domain for certificate lookups:
|
|
195
255
|
|
|
196
|
-
|
|
256
|
+
```
|
|
257
|
+
subdomain.example.com → certificate for example.com ✅
|
|
258
|
+
*.example.com → certificate for example.com ✅
|
|
259
|
+
a.b.example.com → not supported (4+ levels) ❌
|
|
260
|
+
```
|
|
197
261
|
|
|
198
|
-
|
|
199
|
-
// Get certificate for domain (no wildcard)
|
|
200
|
-
const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain);
|
|
201
|
-
console.log('Certificate:', myCert);
|
|
202
|
-
|
|
203
|
-
// Get certificate with wildcard (requires DNS-01 handler)
|
|
204
|
-
const certWithWildcard = await smartAcmeInstance.getCertificateForDomain(myDomain, { includeWildcard: true });
|
|
205
|
-
console.log('Certificate with wildcard:', certWithWildcard);
|
|
262
|
+
## Environment
|
|
206
263
|
|
|
207
|
-
|
|
208
|
-
|
|
264
|
+
| Environment | Description |
|
|
265
|
+
|----------------|-------------|
|
|
266
|
+
| `production` | Let's Encrypt production servers. Certificates are browser-trusted. [Rate limits](https://letsencrypt.org/docs/rate-limits/) apply. |
|
|
267
|
+
| `integration` | Let's Encrypt staging servers. No rate limits, but certificates are **not** browser-trusted. Use for testing. |
|
|
209
268
|
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
## Built-in Challenge Handlers
|
|
214
|
-
|
|
215
|
-
This module includes three out-of-the-box ACME challenge handlers:
|
|
216
|
-
|
|
217
|
-
- **Dns01Handler**
|
|
218
|
-
- Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation.
|
|
219
|
-
- Import path:
|
|
220
|
-
```typescript
|
|
221
|
-
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
|
|
222
|
-
```
|
|
223
|
-
- Example:
|
|
224
|
-
```typescript
|
|
225
|
-
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
226
|
-
const cfAccount = new cloudflare.CloudflareAccount('CF_TOKEN');
|
|
227
|
-
const dnsHandler = new Dns01Handler(cfAccount);
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
- **Http01Webroot**
|
|
231
|
-
- Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup.
|
|
232
|
-
- Import path:
|
|
233
|
-
```typescript
|
|
234
|
-
import { Http01Webroot } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js';
|
|
235
|
-
```
|
|
236
|
-
- Example:
|
|
237
|
-
```typescript
|
|
238
|
-
const httpHandler = new Http01Webroot({ webroot: '/var/www/html' });
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
- **Http01MemoryHandler**
|
|
242
|
-
- In-memory HTTP-01 challenge handler that stores and serves ACME tokens without disk I/O.
|
|
243
|
-
- Import path:
|
|
244
|
-
```typescript
|
|
245
|
-
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
|
|
246
|
-
```
|
|
247
|
-
- Example (Express integration):
|
|
248
|
-
```typescript
|
|
249
|
-
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
|
|
250
|
-
const memoryHandler = new Http01MemoryHandler();
|
|
251
|
-
app.use((req, res, next) => memoryHandler.handleRequest(req, res, next));
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
All handlers implement the `IChallengeHandler<T>` interface and can be combined in the `challengeHandlers` array.
|
|
255
|
-
|
|
256
|
-
## Creating Custom Handlers
|
|
257
|
-
|
|
258
|
-
To support additional challenge types or custom validation flows, implement the `IChallengeHandler<T>` interface:
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
import type { IChallengeHandler } from '@push.rocks/smartacme/ts/handlers/IChallengeHandler.js';
|
|
262
|
-
|
|
263
|
-
// Define your custom challenge payload type
|
|
264
|
-
interface MyChallenge { type: string; /* ... */ }
|
|
265
|
-
|
|
266
|
-
class MyCustomHandler implements IChallengeHandler<MyChallenge> {
|
|
267
|
-
getSupportedTypes(): string[] {
|
|
268
|
-
return ['my-01'];
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Prepare the challenge (set DNS records, start servers, etc.)
|
|
272
|
-
async prepare(ch: MyChallenge): Promise<void> {
|
|
273
|
-
// preparation logic
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Optional verify step after prepare
|
|
277
|
-
async verify?(ch: MyChallenge): Promise<void> {
|
|
278
|
-
// verification logic
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Cleanup after challenge (remove records, stop servers)
|
|
282
|
-
async cleanup(ch: MyChallenge): Promise<void> {
|
|
283
|
-
// cleanup logic
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Then register your handler:
|
|
288
|
-
const customInstance = new SmartAcme({
|
|
289
|
-
/* other options */,
|
|
290
|
-
challengeHandlers: [ new MyCustomHandler() ],
|
|
291
|
-
challengePriority: ['my-01'],
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
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.
|
|
295
|
-
|
|
296
|
-
## Additional Details
|
|
297
|
-
|
|
298
|
-
### Certificate Object
|
|
299
|
-
|
|
300
|
-
The certificate object obtained from the `getCertificateForDomain` method has the following properties:
|
|
301
|
-
|
|
302
|
-
- `id`: Unique identifier for the certificate.
|
|
303
|
-
- `domainName`: The domain name for which the certificate is issued.
|
|
304
|
-
- `created`: Timestamp of when the certificate was created.
|
|
305
|
-
- `privateKey`: The private key associated with the certificate.
|
|
306
|
-
- `publicKey`: The public key or certificate itself.
|
|
307
|
-
- `csr`: Certificate Signing Request (CSR) used to obtain the certificate.
|
|
308
|
-
- `validUntil`: Timestamp indicating the expiration date of the certificate.
|
|
309
|
-
|
|
310
|
-
### Methods Summary
|
|
311
|
-
|
|
312
|
-
- **start()**: Initializes the SmartAcme instance, sets up the ACME client, and registers the account with Let's Encrypt.
|
|
313
|
-
- **stop()**: Closes the MongoDB connection and performs any necessary cleanup.
|
|
314
|
-
- **getCertificateForDomain(domainArg: string, options?: { includeWildcard?: boolean })**: 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.
|
|
315
|
-
- By default, only a certificate for the exact domain is requested
|
|
316
|
-
- Set `includeWildcard: true` to also request a wildcard certificate (requires DNS-01 handler)
|
|
317
|
-
- When requesting a wildcard directly (e.g., `*.example.com`), only the wildcard certificate is requested
|
|
318
|
-
|
|
319
|
-
### Handling Domain Matching
|
|
320
|
-
|
|
321
|
-
The `SmartacmeCertMatcher` class is responsible for matching certificates with the broadest scope for wildcard certificates. The `getCertificateDomainNameByDomainName` method ensures that domains at various levels are correctly matched.
|
|
269
|
+
## Complete Example with HTTP-01
|
|
322
270
|
|
|
323
271
|
```typescript
|
|
324
|
-
import {
|
|
272
|
+
import { SmartAcme, certmanagers, handlers } from '@push.rocks/smartacme';
|
|
273
|
+
import * as http from 'http';
|
|
325
274
|
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
console.log('Certificate Domain Name:', certDomainName); // Output: example.com
|
|
329
|
-
```
|
|
275
|
+
// In-memory handler for HTTP-01 challenges
|
|
276
|
+
const memHandler = new handlers.Http01MemoryHandler();
|
|
330
277
|
|
|
331
|
-
|
|
278
|
+
// Create HTTP server that serves ACME challenges
|
|
279
|
+
const server = http.createServer((req, res) => {
|
|
280
|
+
memHandler.handleRequest(req, res, () => {
|
|
281
|
+
res.statusCode = 200;
|
|
282
|
+
res.end('OK');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
server.listen(80);
|
|
332
286
|
|
|
333
|
-
|
|
287
|
+
// Set up SmartAcme with in-memory storage and HTTP-01
|
|
288
|
+
const smartAcme = new SmartAcme({
|
|
289
|
+
accountEmail: 'admin@example.com',
|
|
290
|
+
certManager: new certmanagers.MemoryCertManager(),
|
|
291
|
+
environment: 'production',
|
|
292
|
+
challengeHandlers: [memHandler],
|
|
293
|
+
challengePriority: ['http-01'],
|
|
294
|
+
});
|
|
334
295
|
|
|
335
|
-
|
|
336
|
-
|
|
296
|
+
await smartAcme.start();
|
|
297
|
+
|
|
298
|
+
const cert = await smartAcme.getCertificateForDomain('example.com');
|
|
299
|
+
// Use cert.publicKey and cert.privateKey with your HTTPS server
|
|
300
|
+
|
|
301
|
+
await smartAcme.stop();
|
|
302
|
+
server.close();
|
|
337
303
|
```
|
|
338
304
|
|
|
305
|
+
## Architecture
|
|
339
306
|
|
|
340
|
-
|
|
307
|
+
Under the hood, SmartAcme uses a fully custom RFC 8555-compliant ACME protocol implementation (no external ACME libraries). Key internal modules:
|
|
341
308
|
|
|
342
|
-
|
|
309
|
+
| Module | Purpose |
|
|
310
|
+
|--------|---------|
|
|
311
|
+
| `AcmeClient` | Top-level ACME facade — orders, authorizations, finalization |
|
|
312
|
+
| `AcmeCrypto` | RSA key generation, JWK/JWS (RFC 7515/7638), CSR via `@peculiar/x509` |
|
|
313
|
+
| `AcmeHttpClient` | JWS-signed HTTP transport with nonce management and structured logging |
|
|
314
|
+
| `AcmeError` | Structured error class with type URN, subproblems, Retry-After, retryability |
|
|
315
|
+
| `AcmeOrderManager` | Order lifecycle — create, poll, finalize, download certificate |
|
|
316
|
+
| `AcmeChallengeManager` | Key authorization computation and challenge completion |
|
|
317
|
+
|
|
318
|
+
All cryptographic operations use `node:crypto`. The only external crypto dependency is `@peculiar/x509` for CSR generation.
|
|
343
319
|
|
|
344
320
|
## License and Legal Information
|
|
345
321
|
|
|
346
|
-
This repository contains open-source code
|
|
322
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
347
323
|
|
|
348
324
|
**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.
|
|
349
325
|
|
|
350
326
|
### Trademarks
|
|
351
327
|
|
|
352
|
-
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 and are not included within the scope of the MIT license granted herein.
|
|
328
|
+
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.
|
|
329
|
+
|
|
330
|
+
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.
|
|
353
331
|
|
|
354
332
|
### Company Information
|
|
355
333
|
|
|
356
334
|
Task Venture Capital GmbH
|
|
357
|
-
Registered at District
|
|
335
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
358
336
|
|
|
359
|
-
For any legal inquiries or
|
|
337
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
360
338
|
|
|
361
339
|
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.
|
package/ts/00_commitinfo_data.ts
CHANGED