@push.rocks/smartacme 7.3.4 → 9.0.0
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 +68 -0
- package/dist_ts/acme/acme.classes.client.js +71 -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 +36 -0
- package/dist_ts/acme/acme.classes.http-client.js +201 -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 +7 -2
- package/dist_ts/smartacme.classes.smartacme.js +80 -20
- package/npmextra.json +12 -6
- package/package.json +23 -24
- package/readme.hints.md +37 -2
- package/readme.md +201 -255
- 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 +99 -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 +236 -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 +88 -23
- 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,353 +1,299 @@
|
|
|
1
1
|
# @push.rocks/smartacme
|
|
2
2
|
|
|
3
|
-
A TypeScript-based ACME client
|
|
3
|
+
A TypeScript-based ACME client for LetsEncrypt 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 supports pluggable challenge handlers (DNS-01, HTTP-01) and pluggable certificate storage backends (MongoDB, in-memory, or your own).
|
|
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
|
|
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'; // LetsEncrypt 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;
|
|
69
|
+
factor?: number;
|
|
70
|
+
minTimeoutMs?: number;
|
|
71
|
+
maxTimeoutMs?: number;
|
|
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');
|
|
81
|
+
|
|
82
|
+
// Include wildcard certificate (requires DNS-01 handler)
|
|
83
|
+
const certWithWildcard = await smartAcme.getCertificateForDomain('example.com', {
|
|
84
|
+
includeWildcard: true,
|
|
85
|
+
});
|
|
29
86
|
|
|
30
|
-
|
|
87
|
+
// Request wildcard only
|
|
88
|
+
const wildcardCert = await smartAcme.getCertificateForDomain('*.example.com');
|
|
89
|
+
```
|
|
31
90
|
|
|
32
|
-
|
|
91
|
+
Certificates are automatically cached and reused when still valid. Renewal happens automatically when a certificate is within 10 days of expiration.
|
|
33
92
|
|
|
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)
|
|
93
|
+
### Certificate Object
|
|
42
94
|
|
|
43
|
-
|
|
95
|
+
The returned `SmartacmeCert` object has these properties:
|
|
44
96
|
|
|
45
|
-
|
|
97
|
+
| Property | Type | Description |
|
|
98
|
+
|-------------|----------|--------------------------------------|
|
|
99
|
+
| `id` | `string` | Unique certificate identifier |
|
|
100
|
+
| `domainName`| `string` | Domain the cert is issued for |
|
|
101
|
+
| `publicKey` | `string` | PEM-encoded certificate |
|
|
102
|
+
| `privateKey`| `string` | PEM-encoded private key |
|
|
103
|
+
| `csr` | `string` | Certificate Signing Request |
|
|
104
|
+
| `created` | `number` | Timestamp of creation |
|
|
105
|
+
| `validUntil`| `number` | Timestamp of expiration |
|
|
46
106
|
|
|
47
|
-
|
|
107
|
+
## Certificate Managers
|
|
48
108
|
|
|
49
|
-
|
|
109
|
+
SmartAcme uses the `ICertManager` interface for pluggable certificate storage.
|
|
50
110
|
|
|
51
|
-
|
|
52
|
-
import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
|
|
53
|
-
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
54
|
-
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
|
|
111
|
+
### MongoCertManager
|
|
55
112
|
|
|
56
|
-
|
|
57
|
-
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
|
|
113
|
+
Persistent storage backed by MongoDB using `@push.rocks/smartdata`:
|
|
58
114
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
mongoDbUrl: 'mongodb://yourmongoURL',
|
|
62
|
-
mongoDbName: 'yourDbName',
|
|
63
|
-
mongoDbPass: 'yourDbPassword',
|
|
64
|
-
});
|
|
115
|
+
```typescript
|
|
116
|
+
import { certmanagers } from '@push.rocks/smartacme';
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
environment: 'integration', // 'production' to request real certificates
|
|
71
|
-
retryOptions: {}, // optional retry/backoff settings
|
|
72
|
-
challengeHandlers: [ // pluggable ACME challenge handlers
|
|
73
|
-
new Dns01Handler(cfAccount),
|
|
74
|
-
// add more handlers as needed (e.g., Http01Webroot, Http01MemoryHandler)
|
|
75
|
-
],
|
|
76
|
-
challengePriority: ['dns-01'], // optional challenge ordering
|
|
118
|
+
const certManager = new certmanagers.MongoCertManager({
|
|
119
|
+
mongoDbUrl: 'mongodb://localhost:27017',
|
|
120
|
+
mongoDbName: 'myapp',
|
|
121
|
+
mongoDbPass: 'secret',
|
|
77
122
|
});
|
|
78
123
|
```
|
|
79
124
|
|
|
80
|
-
###
|
|
125
|
+
### MemoryCertManager
|
|
81
126
|
|
|
82
|
-
|
|
127
|
+
In-memory storage, ideal for testing or ephemeral workloads:
|
|
83
128
|
|
|
84
129
|
```typescript
|
|
85
|
-
|
|
130
|
+
import { certmanagers } from '@push.rocks/smartacme';
|
|
131
|
+
|
|
132
|
+
const certManager = new certmanagers.MemoryCertManager();
|
|
86
133
|
```
|
|
87
134
|
|
|
88
|
-
###
|
|
135
|
+
### Custom Certificate Manager
|
|
89
136
|
|
|
90
|
-
|
|
137
|
+
Implement the `ICertManager` interface for your own storage backend:
|
|
91
138
|
|
|
92
139
|
```typescript
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
140
|
+
import type { ICertManager } from '@push.rocks/smartacme';
|
|
141
|
+
import { Cert } from '@push.rocks/smartacme';
|
|
142
|
+
|
|
143
|
+
class RedisCertManager implements ICertManager {
|
|
144
|
+
async init(): Promise<void> { /* connect */ }
|
|
145
|
+
async retrieveCertificate(domainName: string): Promise<Cert | null> { /* lookup */ }
|
|
146
|
+
async storeCertificate(cert: Cert): Promise<void> { /* save */ }
|
|
147
|
+
async deleteCertificate(domainName: string): Promise<void> { /* remove */ }
|
|
148
|
+
async close(): Promise<void> { /* disconnect */ }
|
|
149
|
+
async wipe(): Promise<void> { /* clear all */ }
|
|
150
|
+
}
|
|
96
151
|
```
|
|
97
152
|
|
|
98
|
-
|
|
153
|
+
## Challenge Handlers
|
|
99
154
|
|
|
100
|
-
SmartAcme
|
|
155
|
+
SmartAcme ships with three built-in ACME challenge handlers. All implement `IChallengeHandler<T>`.
|
|
101
156
|
|
|
102
|
-
###
|
|
157
|
+
### 🌐 Dns01Handler
|
|
103
158
|
|
|
104
|
-
|
|
159
|
+
Uses Cloudflare (or any `IConvenientDnsProvider`) to set and remove DNS TXT records for `dns-01` challenges:
|
|
105
160
|
|
|
106
161
|
```typescript
|
|
107
|
-
import {
|
|
162
|
+
import { handlers } from '@push.rocks/smartacme';
|
|
163
|
+
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
108
164
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
mongoDbName: 'yourDbName',
|
|
112
|
-
mongoDbPass: 'yourDbPassword',
|
|
113
|
-
});
|
|
165
|
+
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
|
|
166
|
+
const dnsHandler = new handlers.Dns01Handler(cfAccount);
|
|
114
167
|
```
|
|
115
168
|
|
|
116
|
-
|
|
169
|
+
DNS-01 is required for wildcard certificates and works regardless of server accessibility.
|
|
117
170
|
|
|
118
|
-
|
|
119
|
-
- In-memory storage, suitable for testing or ephemeral use.
|
|
120
|
-
- Import example:
|
|
121
|
-
```typescript
|
|
122
|
-
import { MemoryCertManager } from '@push.rocks/smartacme';
|
|
123
|
-
const certManager = new MemoryCertManager();
|
|
124
|
-
```
|
|
171
|
+
### 📁 Http01Webroot
|
|
125
172
|
|
|
126
|
-
-
|
|
127
|
-
- Persistent storage in MongoDB (collection: `SmartacmeCert`).
|
|
128
|
-
- Import example:
|
|
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
|
-
```
|
|
137
|
-
|
|
138
|
-
#### Custom Certificate Managers
|
|
139
|
-
|
|
140
|
-
To implement a custom certificate manager, implement the `ICertManager` interface and pass it to `SmartAcme`:
|
|
173
|
+
Writes challenge response files to a filesystem webroot for `http-01` validation:
|
|
141
174
|
|
|
142
175
|
```typescript
|
|
143
|
-
import
|
|
144
|
-
import { SmartAcme } from '@push.rocks/smartacme';
|
|
145
|
-
|
|
146
|
-
class MyCustomCertManager implements ICertManager {
|
|
147
|
-
async init(): Promise<void> { /* setup storage */ }
|
|
148
|
-
async get(domainName: string): Promise<SmartacmeCert | null> { /* lookup cert */ }
|
|
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 */ }
|
|
152
|
-
}
|
|
176
|
+
import { handlers } from '@push.rocks/smartacme';
|
|
153
177
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const smartAcme = new SmartAcme({
|
|
157
|
-
accountEmail: 'youremail@example.com',
|
|
158
|
-
certManager: customManager,
|
|
159
|
-
environment: 'integration',
|
|
160
|
-
challengeHandlers: [], // add your handlers
|
|
178
|
+
const httpHandler = new handlers.Http01Webroot({
|
|
179
|
+
webroot: '/var/www/html',
|
|
161
180
|
});
|
|
162
181
|
```
|
|
163
182
|
|
|
164
|
-
|
|
183
|
+
The handler writes to `<webroot>/.well-known/acme-challenge/<token>` and cleans up after validation.
|
|
165
184
|
|
|
166
|
-
|
|
185
|
+
### 🧠 Http01MemoryHandler
|
|
167
186
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
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:
|
|
187
|
+
In-memory HTTP-01 handler — stores challenge tokens in memory and serves them via `handleRequest()`:
|
|
171
188
|
|
|
172
189
|
```typescript
|
|
173
|
-
import {
|
|
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
|
-
});
|
|
190
|
+
import { handlers } from '@push.rocks/smartacme';
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
const memHandler = new handlers.Http01MemoryHandler();
|
|
193
|
+
|
|
194
|
+
// Integrate with any HTTP server (Express, Koa, raw http, etc.)
|
|
195
|
+
app.use((req, res, next) => memHandler.handleRequest(req, res, next));
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Perfect for serverless or container environments where filesystem access is limited.
|
|
199
|
+
|
|
200
|
+
### Custom Challenge Handler
|
|
201
|
+
|
|
202
|
+
Implement `IChallengeHandler<T>` for custom challenge types:
|
|
195
203
|
|
|
196
|
-
|
|
204
|
+
```typescript
|
|
205
|
+
import type { IChallengeHandler } from '@push.rocks/smartacme';
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
interface MyChallenge {
|
|
208
|
+
type: string;
|
|
209
|
+
token: string;
|
|
210
|
+
keyAuthorization: string;
|
|
211
|
+
}
|
|
201
212
|
|
|
202
|
-
|
|
213
|
+
class MyHandler implements IChallengeHandler<MyChallenge> {
|
|
214
|
+
getSupportedTypes(): string[] { return ['http-01']; }
|
|
215
|
+
async prepare(ch: MyChallenge): Promise<void> { /* ... */ }
|
|
216
|
+
async cleanup(ch: MyChallenge): Promise<void> { /* ... */ }
|
|
217
|
+
async checkWetherDomainIsSupported(domain: string): Promise<boolean> { return true; }
|
|
203
218
|
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Domain Matching
|
|
222
|
+
|
|
223
|
+
SmartAcme automatically maps subdomains to their base domain for certificate lookups:
|
|
204
224
|
|
|
205
|
-
|
|
225
|
+
```typescript
|
|
226
|
+
// subdomain.example.com → certificate for example.com
|
|
227
|
+
// *.example.com → certificate for example.com
|
|
228
|
+
// a.b.example.com → not supported (4+ level domains)
|
|
206
229
|
```
|
|
207
|
-
|
|
208
|
-
## Built-in Challenge Handlers
|
|
209
|
-
|
|
210
|
-
This module includes three out-of-the-box ACME challenge handlers:
|
|
211
|
-
|
|
212
|
-
- **Dns01Handler**
|
|
213
|
-
- Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation.
|
|
214
|
-
- Import path:
|
|
215
|
-
```typescript
|
|
216
|
-
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
|
|
217
|
-
```
|
|
218
|
-
- Example:
|
|
219
|
-
```typescript
|
|
220
|
-
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
221
|
-
const cfAccount = new cloudflare.CloudflareAccount('CF_TOKEN');
|
|
222
|
-
const dnsHandler = new Dns01Handler(cfAccount);
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
- **Http01Webroot**
|
|
226
|
-
- Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup.
|
|
227
|
-
- Import path:
|
|
228
|
-
```typescript
|
|
229
|
-
import { Http01Webroot } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js';
|
|
230
|
-
```
|
|
231
|
-
- Example:
|
|
232
|
-
```typescript
|
|
233
|
-
const httpHandler = new Http01Webroot({ webroot: '/var/www/html' });
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
- **Http01MemoryHandler**
|
|
237
|
-
- In-memory HTTP-01 challenge handler that stores and serves ACME tokens without disk I/O.
|
|
238
|
-
- Import path:
|
|
239
|
-
```typescript
|
|
240
|
-
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
|
|
241
|
-
```
|
|
242
|
-
- Example (Express integration):
|
|
243
|
-
```typescript
|
|
244
|
-
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
|
|
245
|
-
const memoryHandler = new Http01MemoryHandler();
|
|
246
|
-
app.use((req, res, next) => memoryHandler.handleRequest(req, res, next));
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
All handlers implement the `IChallengeHandler<T>` interface and can be combined in the `challengeHandlers` array.
|
|
250
|
-
|
|
251
|
-
## Creating Custom Handlers
|
|
252
|
-
|
|
253
|
-
To support additional challenge types or custom validation flows, implement the `IChallengeHandler<T>` interface:
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
import type { IChallengeHandler } from '@push.rocks/smartacme/ts/handlers/IChallengeHandler.js';
|
|
257
|
-
|
|
258
|
-
// Define your custom challenge payload type
|
|
259
|
-
interface MyChallenge { type: string; /* ... */ }
|
|
260
|
-
|
|
261
|
-
class MyCustomHandler implements IChallengeHandler<MyChallenge> {
|
|
262
|
-
getSupportedTypes(): string[] {
|
|
263
|
-
return ['my-01'];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Prepare the challenge (set DNS records, start servers, etc.)
|
|
267
|
-
async prepare(ch: MyChallenge): Promise<void> {
|
|
268
|
-
// preparation logic
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Optional verify step after prepare
|
|
272
|
-
async verify?(ch: MyChallenge): Promise<void> {
|
|
273
|
-
// verification logic
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Cleanup after challenge (remove records, stop servers)
|
|
277
|
-
async cleanup(ch: MyChallenge): Promise<void> {
|
|
278
|
-
// cleanup logic
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Then register your handler:
|
|
283
|
-
const customInstance = new SmartAcme({
|
|
284
|
-
/* other options */,
|
|
285
|
-
challengeHandlers: [ new MyCustomHandler() ],
|
|
286
|
-
challengePriority: ['my-01'],
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
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.
|
|
290
|
-
|
|
291
|
-
## Additional Details
|
|
292
230
|
|
|
293
|
-
|
|
231
|
+
## Environment
|
|
294
232
|
|
|
295
|
-
|
|
233
|
+
- **`production`** — Uses LetsEncrypt production servers. Rate limits apply.
|
|
234
|
+
- **`integration`** — Uses LetsEncrypt staging servers. No rate limits, but certificates are not trusted by browsers. Use for testing.
|
|
296
235
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
- `csr`: Certificate Signing Request (CSR) used to obtain the certificate.
|
|
303
|
-
- `validUntil`: Timestamp indicating the expiration date of the certificate.
|
|
236
|
+
## Complete Example with HTTP-01
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { SmartAcme, certmanagers, handlers } from '@push.rocks/smartacme';
|
|
240
|
+
import * as http from 'http';
|
|
304
241
|
|
|
305
|
-
|
|
242
|
+
// In-memory handler for HTTP-01 challenges
|
|
243
|
+
const memHandler = new handlers.Http01MemoryHandler();
|
|
306
244
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
245
|
+
// Create HTTP server that serves ACME challenges
|
|
246
|
+
const server = http.createServer((req, res) => {
|
|
247
|
+
memHandler.handleRequest(req, res, () => {
|
|
248
|
+
res.statusCode = 200;
|
|
249
|
+
res.end('OK');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
server.listen(80);
|
|
310
253
|
|
|
311
|
-
|
|
254
|
+
// Set up SmartAcme with in-memory storage and HTTP-01
|
|
255
|
+
const smartAcme = new SmartAcme({
|
|
256
|
+
accountEmail: 'admin@example.com',
|
|
257
|
+
certManager: new certmanagers.MemoryCertManager(),
|
|
258
|
+
environment: 'production',
|
|
259
|
+
challengeHandlers: [memHandler],
|
|
260
|
+
challengePriority: ['http-01'],
|
|
261
|
+
});
|
|
312
262
|
|
|
313
|
-
|
|
263
|
+
await smartAcme.start();
|
|
314
264
|
|
|
315
|
-
|
|
316
|
-
|
|
265
|
+
const cert = await smartAcme.getCertificateForDomain('example.com');
|
|
266
|
+
// Use cert.publicKey and cert.privateKey with your HTTPS server
|
|
317
267
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
console.log('Certificate Domain Name:', certDomainName); // Output: example.com
|
|
268
|
+
await smartAcme.stop();
|
|
269
|
+
server.close();
|
|
321
270
|
```
|
|
322
271
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
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:
|
|
272
|
+
## Testing
|
|
326
273
|
|
|
327
274
|
```bash
|
|
328
275
|
pnpm test
|
|
329
276
|
```
|
|
330
277
|
|
|
331
|
-
|
|
332
|
-
This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`.
|
|
333
|
-
|
|
334
|
-
---
|
|
278
|
+
Tests use `@git.zone/tstest` with the tapbundle assertion library.
|
|
335
279
|
|
|
336
280
|
## License and Legal Information
|
|
337
281
|
|
|
338
|
-
This repository contains open-source code
|
|
282
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
339
283
|
|
|
340
284
|
**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.
|
|
341
285
|
|
|
342
286
|
### Trademarks
|
|
343
287
|
|
|
344
|
-
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.
|
|
288
|
+
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.
|
|
289
|
+
|
|
290
|
+
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.
|
|
345
291
|
|
|
346
292
|
### Company Information
|
|
347
293
|
|
|
348
294
|
Task Venture Capital GmbH
|
|
349
|
-
Registered at District
|
|
295
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
350
296
|
|
|
351
|
-
For any legal inquiries or
|
|
297
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
352
298
|
|
|
353
299
|
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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AcmeHttpClient } from './acme.classes.http-client.js';
|
|
2
|
+
import type { IAcmeAccount, IAcmeAccountCreateRequest } from './acme.interfaces.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ACME account management - registration and key management
|
|
6
|
+
*/
|
|
7
|
+
export class AcmeAccount {
|
|
8
|
+
private httpClient: AcmeHttpClient;
|
|
9
|
+
private accountUrl: string | null = null;
|
|
10
|
+
|
|
11
|
+
constructor(httpClient: AcmeHttpClient) {
|
|
12
|
+
this.httpClient = httpClient;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register or retrieve an ACME account.
|
|
17
|
+
* Uses JWK (not kid) since account URL is not yet known.
|
|
18
|
+
* Captures account URL from Location header for subsequent requests.
|
|
19
|
+
*/
|
|
20
|
+
async create(request: IAcmeAccountCreateRequest): Promise<IAcmeAccount> {
|
|
21
|
+
const dir = await this.httpClient.getDirectory();
|
|
22
|
+
const response = await this.httpClient.signedRequest(dir.newAccount, request, {
|
|
23
|
+
useJwk: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Capture account URL from Location header (used as kid for future requests)
|
|
27
|
+
const location = response.headers['location'];
|
|
28
|
+
if (location) {
|
|
29
|
+
this.accountUrl = location;
|
|
30
|
+
this.httpClient.kid = location;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return response.data as IAcmeAccount;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the account URL (kid) for use in JWS headers
|
|
38
|
+
*/
|
|
39
|
+
getAccountUrl(): string {
|
|
40
|
+
if (!this.accountUrl) {
|
|
41
|
+
throw new Error('Account not yet created - call create() first');
|
|
42
|
+
}
|
|
43
|
+
return this.accountUrl;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { AcmeCrypto } from './acme.classes.crypto.js';
|
|
3
|
+
import type { AcmeHttpClient } from './acme.classes.http-client.js';
|
|
4
|
+
import type { IAcmeChallenge } from './acme.interfaces.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ACME challenge operations - key authorization computation and challenge completion
|
|
8
|
+
*/
|
|
9
|
+
export class AcmeChallengeManager {
|
|
10
|
+
private httpClient: AcmeHttpClient;
|
|
11
|
+
private accountKeyPem: string;
|
|
12
|
+
|
|
13
|
+
constructor(httpClient: AcmeHttpClient, accountKeyPem: string) {
|
|
14
|
+
this.httpClient = httpClient;
|
|
15
|
+
this.accountKeyPem = accountKeyPem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compute the key authorization for a challenge.
|
|
20
|
+
* For http-01: returns `token.thumbprint`
|
|
21
|
+
* For dns-01: returns `base64url(sha256(token.thumbprint))`
|
|
22
|
+
*
|
|
23
|
+
* This is a synchronous, pure-crypto computation.
|
|
24
|
+
*/
|
|
25
|
+
getKeyAuthorization(challenge: IAcmeChallenge): string {
|
|
26
|
+
const jwk = AcmeCrypto.getJwk(this.accountKeyPem);
|
|
27
|
+
const thumbprint = AcmeCrypto.getJwkThumbprint(jwk);
|
|
28
|
+
const keyAuth = `${challenge.token}.${thumbprint}`;
|
|
29
|
+
|
|
30
|
+
if (challenge.type === 'dns-01') {
|
|
31
|
+
// DNS-01 uses base64url(SHA-256(keyAuthorization))
|
|
32
|
+
return crypto.createHash('sha256').update(keyAuth).digest().toString('base64url');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// HTTP-01 and others use the raw key authorization
|
|
36
|
+
return keyAuth;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Notify the ACME server to validate a challenge (POST {} to challenge URL)
|
|
41
|
+
*/
|
|
42
|
+
async complete(challenge: IAcmeChallenge): Promise<void> {
|
|
43
|
+
await this.httpClient.signedRequest(challenge.url, {});
|
|
44
|
+
}
|
|
45
|
+
}
|