@push.rocks/smartacme 8.0.0 → 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.
Files changed (50) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/acme/acme.classes.account.d.ts +20 -0
  3. package/dist_ts/acme/acme.classes.account.js +37 -0
  4. package/dist_ts/acme/acme.classes.challenge.d.ts +22 -0
  5. package/dist_ts/acme/acme.classes.challenge.js +36 -0
  6. package/dist_ts/acme/acme.classes.client.d.ts +68 -0
  7. package/dist_ts/acme/acme.classes.client.js +71 -0
  8. package/dist_ts/acme/acme.classes.crypto.d.ts +50 -0
  9. package/dist_ts/acme/acme.classes.crypto.js +180 -0
  10. package/dist_ts/acme/acme.classes.directory.d.ts +13 -0
  11. package/dist_ts/acme/acme.classes.directory.js +14 -0
  12. package/dist_ts/acme/acme.classes.error.d.ts +44 -0
  13. package/dist_ts/acme/acme.classes.error.js +41 -0
  14. package/dist_ts/acme/acme.classes.http-client.d.ts +36 -0
  15. package/dist_ts/acme/acme.classes.http-client.js +201 -0
  16. package/dist_ts/acme/acme.classes.order.d.ts +40 -0
  17. package/dist_ts/acme/acme.classes.order.js +100 -0
  18. package/dist_ts/acme/acme.interfaces.d.ts +64 -0
  19. package/dist_ts/acme/acme.interfaces.js +5 -0
  20. package/dist_ts/acme/index.d.ts +5 -0
  21. package/dist_ts/acme/index.js +5 -0
  22. package/dist_ts/certmanagers/mongo.js +1 -2
  23. package/dist_ts/plugins.d.ts +2 -7
  24. package/dist_ts/plugins.js +5 -11
  25. package/dist_ts/smartacme.classes.smartacme.d.ts +3 -1
  26. package/dist_ts/smartacme.classes.smartacme.js +40 -13
  27. package/npmextra.json +12 -6
  28. package/package.json +23 -24
  29. package/readme.hints.md +27 -3
  30. package/readme.md +201 -263
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/acme/acme.classes.account.ts +45 -0
  33. package/ts/acme/acme.classes.challenge.ts +45 -0
  34. package/ts/acme/acme.classes.client.ts +99 -0
  35. package/ts/acme/acme.classes.crypto.ts +220 -0
  36. package/ts/acme/acme.classes.directory.ts +13 -0
  37. package/ts/acme/acme.classes.error.ts +55 -0
  38. package/ts/acme/acme.classes.http-client.ts +236 -0
  39. package/ts/acme/acme.classes.order.ts +125 -0
  40. package/ts/acme/acme.interfaces.ts +74 -0
  41. package/ts/acme/index.ts +16 -0
  42. package/ts/certmanagers/mongo.ts +0 -1
  43. package/ts/plugins.ts +3 -14
  44. package/ts/smartacme.classes.smartacme.ts +41 -16
  45. package/dist_ts/certmanagers.d.ts +0 -42
  46. package/dist_ts/certmanagers.js +0 -86
  47. package/dist_ts/smartacme.classes.certmanager.d.ts +0 -43
  48. package/dist_ts/smartacme.classes.certmanager.js +0 -92
  49. package/dist_ts/smartacme.plugins.d.ts +0 -21
  50. package/dist_ts/smartacme.plugins.js +0 -28
package/readme.md CHANGED
@@ -1,361 +1,299 @@
1
1
  # @push.rocks/smartacme
2
2
 
3
- A TypeScript-based ACME client with an easy yet powerful interface for LetsEncrypt certificate management.
3
+ A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power. 🔒
4
4
 
5
- ## Install
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
- Using pnpm as the package manager:
9
+ ## Install
8
10
 
9
11
  ```bash
10
12
  pnpm add @push.rocks/smartacme
11
13
  ```
12
14
 
13
- Ensure your project is set up to use TypeScript and ECMAScript Modules (ESM).
14
- ## Running Tests
15
+ Ensure your project uses TypeScript and ECMAScript Modules (ESM).
15
16
 
16
- Tests are written using `@push.rocks/tapbundle` and can be run with:
17
+ ## Usage
17
18
 
18
- ```bash
19
- pnpm test
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
- To run a specific test file:
57
+ ### SmartAcme Options
23
58
 
24
- ```bash
25
- tsx test/<test-file>.ts
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
- ## Usage
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
- This guide will walk you through using `@push.rocks/smartacme` to set up and manage ACME (Automated Certificate Management Environment) certificates with a focus on the Let's Encrypt service, which provides free SSL certificates. The library provides an easy yet powerful TypeScript interface to automate the process of obtaining, renewing, and installing your SSL certificates.
87
+ // Request wildcard only
88
+ const wildcardCert = await smartAcme.getCertificateForDomain('*.example.com');
89
+ ```
31
90
 
32
- ### Table of Contents
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
- 1. [Setting Up Your Project](#setting-up-your-project)
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
- ### Setting Up Your Project
95
+ The returned `SmartacmeCert` object has these properties:
44
96
 
45
- Ensure your project includes the necessary TypeScript configuration and dependencies. You'll need to have TypeScript installed and configured for ECMAScript Modules. If you are new to TypeScript, review its [documentation](https://www.typescriptlang.org/docs/) to get started.
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
- ### Creating a SmartAcme Instance
107
+ ## Certificate Managers
48
108
 
49
- Start by importing the `SmartAcme` class and any built-in handlers you plan to use. For example, to use DNS-01 via Cloudflare:
109
+ SmartAcme uses the `ICertManager` interface for pluggable certificate storage.
50
110
 
51
- ```typescript
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
- // Create a Cloudflare account client with your API token
57
- const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
113
+ Persistent storage backed by MongoDB using `@push.rocks/smartdata`:
58
114
 
59
- // Initialize a certificate manager (e.g., MongoDB)
60
- const certManager = new MongoCertManager({
61
- mongoDbUrl: 'mongodb://yourmongoURL',
62
- mongoDbName: 'yourDbName',
63
- mongoDbPass: 'yourDbPassword',
64
- });
115
+ ```typescript
116
+ import { certmanagers } from '@push.rocks/smartacme';
65
117
 
66
- // Instantiate SmartAcme with the certManager and challenge handlers
67
- const smartAcmeInstance = new SmartAcme({
68
- accountEmail: 'youremail@example.com',
69
- certManager,
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
- ### Initializing SmartAcme
125
+ ### MemoryCertManager
81
126
 
82
- Before proceeding to request certificates, start your SmartAcme instance:
127
+ In-memory storage, ideal for testing or ephemeral workloads:
83
128
 
84
129
  ```typescript
85
- await smartAcmeInstance.start();
130
+ import { certmanagers } from '@push.rocks/smartacme';
131
+
132
+ const certManager = new certmanagers.MemoryCertManager();
86
133
  ```
87
134
 
88
- ### Obtaining a Certificate for a Domain
135
+ ### Custom Certificate Manager
89
136
 
90
- To obtain a certificate for a specific domain, use the `getCertificateForDomain` method. This function ensures that if a valid certificate is already present, it will be reused; otherwise, a new certificate is obtained:
137
+ Implement the `ICertManager` interface for your own storage backend:
91
138
 
92
139
  ```typescript
93
- const myDomain = 'example.com';
94
- const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain);
95
- console.log('Certificate:', myCert);
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
- ### Automating DNS Challenges
153
+ ## Challenge Handlers
99
154
 
100
- SmartAcme uses pluggable ACME challenge handlers (see built-in handlers below) to automate domain validation. You configure handlers via the `challengeHandlers` array when creating the instance, and SmartAcme will invoke each handler’s `prepare`, optional `verify`, and `cleanup` methods during the ACME order flow.
155
+ SmartAcme ships with three built-in ACME challenge handlers. All implement `IChallengeHandler<T>`.
101
156
 
102
- ### Managing Certificates
157
+ ### 🌐 Dns01Handler
103
158
 
104
- 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.
159
+ Uses Cloudflare (or any `IConvenientDnsProvider`) to set and remove DNS TXT records for `dns-01` challenges:
105
160
 
106
161
  ```typescript
107
- import { MongoCertManager } from '@push.rocks/smartacme';
162
+ import { handlers } from '@push.rocks/smartacme';
163
+ import * as cloudflare from '@apiclient.xyz/cloudflare';
108
164
 
109
- const certManager = new MongoCertManager({
110
- mongoDbUrl: 'mongodb://yourmongoURL',
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
- SmartAcme uses the `ICertManager` interface for certificate storage. Two built-in implementations are available:
169
+ DNS-01 is required for wildcard certificates and works regardless of server accessibility.
117
170
 
118
- - **MemoryCertManager**
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
- - **MongoCertManager**
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 type { ICertManager, Cert as SmartacmeCert } from '@push.rocks/smartacme';
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
- // Use your custom manager:
155
- const customManager = new MyCustomCertManager();
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
- ### Environmental Considerations
183
+ The handler writes to `<webroot>/.well-known/acme-challenge/<token>` and cleans up after validation.
165
184
 
166
- When creating an instance of `SmartAcme`, you can specify an `environment` option. This is particularly useful for testing, as you can use the `integration` environment to avoid hitting rate limits and for testing your setup without issuing real certificates. Switch to `production` when you are ready to obtain actual certificates.
185
+ ### 🧠 Http01MemoryHandler
167
186
 
168
- ### Complete Example
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 { 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
- });
190
+ import { handlers } from '@push.rocks/smartacme';
188
191
 
189
- const smartAcmeInstance = new SmartAcme({
190
- accountEmail: 'youremail@example.com',
191
- certManager,
192
- environment: 'integration',
193
- challengeHandlers: [new Dns01Handler(cloudflareAccount)],
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
- await smartAcmeInstance.start();
204
+ ```typescript
205
+ import type { IChallengeHandler } from '@push.rocks/smartacme';
197
206
 
198
- const myDomain = 'example.com';
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);
207
+ interface MyChallenge {
208
+ type: string;
209
+ token: string;
210
+ keyAuthorization: string;
211
+ }
206
212
 
207
- await smartAcmeInstance.stop();
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; }
208
218
  }
219
+ ```
220
+
221
+ ## Domain Matching
222
+
223
+ SmartAcme automatically maps subdomains to their base domain for certificate lookups:
209
224
 
210
- main().catch(console.error);
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)
211
229
  ```
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
230
 
298
- ### Certificate Object
231
+ ## Environment
299
232
 
300
- The certificate object obtained from the `getCertificateForDomain` method has the following properties:
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.
301
235
 
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.
236
+ ## Complete Example with HTTP-01
237
+
238
+ ```typescript
239
+ import { SmartAcme, certmanagers, handlers } from '@push.rocks/smartacme';
240
+ import * as http from 'http';
309
241
 
310
- ### Methods Summary
242
+ // In-memory handler for HTTP-01 challenges
243
+ const memHandler = new handlers.Http01MemoryHandler();
311
244
 
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
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);
318
253
 
319
- ### Handling Domain Matching
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
+ });
320
262
 
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.
263
+ await smartAcme.start();
322
264
 
323
- ```typescript
324
- import { SmartacmeCertMatcher } from '@push.rocks/smartacme';
265
+ const cert = await smartAcme.getCertificateForDomain('example.com');
266
+ // Use cert.publicKey and cert.privateKey with your HTTPS server
325
267
 
326
- const certMatcher = new SmartacmeCertMatcher();
327
- const certDomainName = certMatcher.getCertificateDomainNameByDomainName('subdomain.example.com');
328
- console.log('Certificate Domain Name:', certDomainName); // Output: example.com
268
+ await smartAcme.stop();
269
+ server.close();
329
270
  ```
330
271
 
331
- ### Testing
332
-
333
- 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
334
273
 
335
274
  ```bash
336
275
  pnpm test
337
276
  ```
338
277
 
339
-
340
- This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`.
341
-
342
- ---
278
+ Tests use `@git.zone/tstest` with the tapbundle assertion library.
343
279
 
344
280
  ## License and Legal Information
345
281
 
346
- This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license.md](license.md) file within this repository.
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.
347
283
 
348
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.
349
285
 
350
286
  ### Trademarks
351
287
 
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. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
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.
353
291
 
354
292
  ### Company Information
355
293
 
356
294
  Task Venture Capital GmbH
357
- Registered at District court Bremen HRB 35230 HB, Germany
295
+ Registered at District Court Bremen HRB 35230 HB, Germany
358
296
 
359
- For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
297
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
360
298
 
361
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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartacme',
6
- version: '8.0.0',
6
+ version: '9.0.0',
7
7
  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
8
8
  }
@@ -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
+ }