@push.rocks/smartacme 5.0.0 → 6.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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (C) 2016, Lossless GmbH
1
+ Copyright (C) 2016, Task Venture Capital GmbH
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
1
1
  /**
2
- * autocreated commitinfo by @pushrocks/commitinfo
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
3
  */
4
4
  export declare const commitinfo: {
5
5
  name: string;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * autocreated commitinfo by @pushrocks/commitinfo
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartacme',
6
- version: '5.0.0',
6
+ version: '6.0.1',
7
7
  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx1QkFBdUI7SUFDN0IsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLDZHQUE2RztDQUMzSCxDQUFBIn0=
@@ -0,0 +1,13 @@
1
+ import * as plugins from '../smartacme.plugins.js';
2
+ import type { IChallengeHandler } from './IChallengeHandler.js';
3
+ /**
4
+ * DNS-01 challenge handler using CloudflareAccount and Smartdns.
5
+ */
6
+ export declare class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.IDnsChallenge> {
7
+ private cf;
8
+ private smartdns;
9
+ constructor(cloudflareAccount: any, smartdnsInstance?: plugins.smartdnsClient.Smartdns);
10
+ getSupportedTypes(): string[];
11
+ prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void>;
12
+ cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void>;
13
+ }
@@ -0,0 +1,24 @@
1
+ import * as plugins from '../smartacme.plugins.js';
2
+ /**
3
+ * DNS-01 challenge handler using CloudflareAccount and Smartdns.
4
+ */
5
+ export class Dns01Handler {
6
+ constructor(cloudflareAccount, smartdnsInstance) {
7
+ this.cf = cloudflareAccount;
8
+ this.smartdns = smartdnsInstance ?? new plugins.smartdnsClient.Smartdns({});
9
+ }
10
+ getSupportedTypes() {
11
+ return ['dns-01'];
12
+ }
13
+ async prepare(ch) {
14
+ // set DNS TXT record
15
+ await this.cf.convenience.acmeSetDnsChallenge(ch);
16
+ // wait for DNS propagation
17
+ await this.smartdns.checkUntilAvailable(ch.hostName, 'TXT', ch.challenge, 100, 5000);
18
+ }
19
+ async cleanup(ch) {
20
+ // remove DNS TXT record
21
+ await this.cf.convenience.acmeRemoveDnsChallenge(ch);
22
+ }
23
+ }
24
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRG5zMDFIYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvaGFuZGxlcnMvRG5zMDFIYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0seUJBQXlCLENBQUM7QUFHbkQ7O0dBRUc7QUFDSCxNQUFNLE9BQU8sWUFBWTtJQUl2QixZQUNFLGlCQUFzQixFQUN0QixnQkFBa0Q7UUFFbEQsSUFBSSxDQUFDLEVBQUUsR0FBRyxpQkFBaUIsQ0FBQztRQUM1QixJQUFJLENBQUMsUUFBUSxHQUFHLGdCQUFnQixJQUFJLElBQUksT0FBTyxDQUFDLGNBQWMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDOUUsQ0FBQztJQUVNLGlCQUFpQjtRQUN0QixPQUFPLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDcEIsQ0FBQztJQUVNLEtBQUssQ0FBQyxPQUFPLENBQUMsRUFBeUM7UUFDNUQscUJBQXFCO1FBQ3JCLE1BQU0sSUFBSSxDQUFDLEVBQUUsQ0FBQyxXQUFXLENBQUMsbUJBQW1CLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDbEQsMkJBQTJCO1FBQzNCLE1BQU0sSUFBSSxDQUFDLFFBQVEsQ0FBQyxtQkFBbUIsQ0FDckMsRUFBRSxDQUFDLFFBQVEsRUFDWCxLQUFLLEVBQ0wsRUFBRSxDQUFDLFNBQVMsRUFDWixHQUFHLEVBQ0gsSUFBSSxDQUNMLENBQUM7SUFDSixDQUFDO0lBRU0sS0FBSyxDQUFDLE9BQU8sQ0FBQyxFQUF5QztRQUM1RCx3QkFBd0I7UUFDeEIsTUFBTSxJQUFJLENBQUMsRUFBRSxDQUFDLFdBQVcsQ0FBQyxzQkFBc0IsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUN2RCxDQUFDO0NBQ0YifQ==
@@ -0,0 +1,34 @@
1
+ import type { IChallengeHandler } from './IChallengeHandler.js';
2
+ /**
3
+ * HTTP-01 ACME challenge handler using file-system webroot.
4
+ * Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/.
5
+ */
6
+ export interface Http01HandlerOptions {
7
+ /**
8
+ * Directory that serves HTTP requests for /.well-known/acme-challenge
9
+ */
10
+ webroot: string;
11
+ }
12
+ export declare class Http01Handler implements IChallengeHandler<{
13
+ type: string;
14
+ token: string;
15
+ keyAuthorization: string;
16
+ webPath: string;
17
+ }> {
18
+ private webroot;
19
+ constructor(options: Http01HandlerOptions);
20
+ getSupportedTypes(): string[];
21
+ prepare(ch: {
22
+ token: string;
23
+ keyAuthorization: string;
24
+ webPath: string;
25
+ }): Promise<void>;
26
+ verify(ch: {
27
+ webPath: string;
28
+ keyAuthorization: string;
29
+ }): Promise<void>;
30
+ cleanup(ch: {
31
+ token: string;
32
+ webPath: string;
33
+ }): Promise<void>;
34
+ }
@@ -0,0 +1,32 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ export class Http01Handler {
4
+ constructor(options) {
5
+ this.webroot = options.webroot;
6
+ }
7
+ getSupportedTypes() {
8
+ return ['http-01'];
9
+ }
10
+ async prepare(ch) {
11
+ const relWebPath = ch.webPath.replace(/^\/+/, '');
12
+ const filePath = path.join(this.webroot, relWebPath);
13
+ const dir = path.dirname(filePath);
14
+ await fs.mkdir(dir, { recursive: true });
15
+ await fs.writeFile(filePath, ch.keyAuthorization, 'utf8');
16
+ }
17
+ async verify(ch) {
18
+ // Optional: implement HTTP polling if desired
19
+ return;
20
+ }
21
+ async cleanup(ch) {
22
+ const relWebPath = ch.webPath.replace(/^\/+/, '');
23
+ const filePath = path.join(this.webroot, relWebPath);
24
+ try {
25
+ await fs.unlink(filePath);
26
+ }
27
+ catch {
28
+ // ignore missing file
29
+ }
30
+ }
31
+ }
32
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiSHR0cDAxSGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2hhbmRsZXJzL0h0dHAwMUhhbmRsZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFFBQVEsSUFBSSxFQUFFLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFDcEMsT0FBTyxLQUFLLElBQUksTUFBTSxNQUFNLENBQUM7QUFjN0IsTUFBTSxPQUFPLGFBQWE7SUFReEIsWUFBWSxPQUE2QjtRQUN2QyxJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUM7SUFDakMsQ0FBQztJQUVNLGlCQUFpQjtRQUN0QixPQUFPLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDckIsQ0FBQztJQUVNLEtBQUssQ0FBQyxPQUFPLENBQUMsRUFBZ0U7UUFDbkYsTUFBTSxVQUFVLEdBQUcsRUFBRSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFDO1FBQ2xELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxVQUFVLENBQUMsQ0FBQztRQUNyRCxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ25DLE1BQU0sRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUN6QyxNQUFNLEVBQUUsQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQyxnQkFBZ0IsRUFBRSxNQUFNLENBQUMsQ0FBQztJQUM1RCxDQUFDO0lBRU0sS0FBSyxDQUFDLE1BQU0sQ0FBQyxFQUFpRDtRQUNuRSw4Q0FBOEM7UUFDOUMsT0FBTztJQUNULENBQUM7SUFFTSxLQUFLLENBQUMsT0FBTyxDQUFDLEVBQXNDO1FBQ3pELE1BQU0sVUFBVSxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNsRCxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsVUFBVSxDQUFDLENBQUM7UUFDckQsSUFBSSxDQUFDO1lBQ0gsTUFBTSxFQUFFLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQzVCLENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCxzQkFBc0I7UUFDeEIsQ0FBQztJQUNILENBQUM7Q0FDRiJ9
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pluggable interface for ACME challenge handlers.
3
+ * Supports DNS-01, HTTP-01, TLS-ALPN-01, or custom challenge types.
4
+ */
5
+ export interface IChallengeHandler<T> {
6
+ /**
7
+ * ACME challenge types this handler supports (e.g. ['dns-01']).
8
+ */
9
+ getSupportedTypes(): string[];
10
+ /**
11
+ * Prepare the challenge: set DNS record, start HTTP/TLS server, etc.
12
+ */
13
+ prepare(ch: T): Promise<void>;
14
+ /**
15
+ * Optional extra verify step (HTTP GET, ALPN handshake).
16
+ */
17
+ verify?(ch: T): Promise<void>;
18
+ /**
19
+ * Clean up resources: remove DNS record, stop server.
20
+ */
21
+ cleanup(ch: T): Promise<void>;
22
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiSUNoYWxsZW5nZUhhbmRsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9oYW5kbGVycy9JQ2hhbGxlbmdlSGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
@@ -0,0 +1,3 @@
1
+ export type { IChallengeHandler } from './IChallengeHandler.js';
2
+ export { Dns01Handler } from './Dns01Handler.js';
3
+ export { Http01Handler } from './Http01Handler.js';
@@ -0,0 +1,4 @@
1
+ // Removed legacy handler adapter
2
+ export { Dns01Handler } from './Dns01Handler.js';
3
+ export { Http01Handler } from './Http01Handler.js';
4
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9oYW5kbGVycy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxpQ0FBaUM7QUFDakMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ2pELE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQyJ9
@@ -7,9 +7,29 @@ export interface ISmartAcmeOptions {
7
7
  accountPrivateKey?: string;
8
8
  accountEmail: string;
9
9
  mongoDescriptor: plugins.smartdata.IMongoDescriptor;
10
- setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
11
- removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
12
10
  environment: 'production' | 'integration';
11
+ /**
12
+ * Optional retry/backoff configuration for transient failures
13
+ */
14
+ retryOptions?: {
15
+ /** number of retry attempts */
16
+ retries?: number;
17
+ /** backoff multiplier */
18
+ factor?: number;
19
+ /** initial delay in milliseconds */
20
+ minTimeoutMs?: number;
21
+ /** maximum delay cap in milliseconds */
22
+ maxTimeoutMs?: number;
23
+ };
24
+ /**
25
+ * Pluggable ACME challenge handlers (DNS-01, HTTP-01, TLS-ALPN-01, etc.)
26
+ */
27
+ challengeHandlers?: plugins.handlers.IChallengeHandler<any>[];
28
+ /**
29
+ * Order of challenge types to try (e.g. ['http-01','dns-01']).
30
+ * Defaults to ['dns-01'] or first supported type from handlers.
31
+ */
32
+ challengePriority?: string[];
13
33
  }
14
34
  /**
15
35
  * class SmartAcme
@@ -27,10 +47,12 @@ export declare class SmartAcme {
27
47
  private smartdns;
28
48
  logger: plugins.smartlog.Smartlog;
29
49
  private privateKey;
30
- private setChallenge;
31
- private removeChallenge;
32
50
  private certmanager;
33
51
  private certmatcher;
52
+ private retryOptions;
53
+ private pendingChallenges;
54
+ private challengeHandlers;
55
+ private challengePriority;
34
56
  constructor(optionsArg: ISmartAcmeOptions);
35
57
  /**
36
58
  * starts the instance
@@ -40,6 +62,12 @@ export declare class SmartAcme {
40
62
  */
41
63
  start(): Promise<void>;
42
64
  stop(): Promise<void>;
65
+ /** Retry helper with exponential backoff */
66
+ private retry;
67
+ /** Clean up pending challenges and shut down */
68
+ private handleShutdown;
69
+ /** Handle process signals for graceful shutdown */
70
+ private handleSignal;
43
71
  /**
44
72
  * gets a certificate
45
73
  * it runs through the following steps
@@ -15,9 +15,30 @@ import { commitinfo } from './00_commitinfo_data.js';
15
15
  */
16
16
  export class SmartAcme {
17
17
  constructor(optionsArg) {
18
- this.smartdns = new plugins.smartdns.Smartdns({});
18
+ this.smartdns = new plugins.smartdnsClient.Smartdns({});
19
+ // track pending DNS challenges for graceful shutdown
20
+ this.pendingChallenges = [];
19
21
  this.options = optionsArg;
20
22
  this.logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
23
+ // enable console output for structured logging
24
+ this.logger.enableConsole();
25
+ // initialize retry/backoff options
26
+ this.retryOptions = {
27
+ retries: optionsArg.retryOptions?.retries ?? 3,
28
+ factor: optionsArg.retryOptions?.factor ?? 2,
29
+ minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
30
+ maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
31
+ };
32
+ // initialize challenge handlers (must provide at least one)
33
+ if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
34
+ throw new Error('You must provide at least one ACME challenge handler via options.challengeHandlers');
35
+ }
36
+ this.challengeHandlers = optionsArg.challengeHandlers;
37
+ // initialize challenge priority
38
+ this.challengePriority =
39
+ optionsArg.challengePriority && optionsArg.challengePriority.length > 0
40
+ ? optionsArg.challengePriority
41
+ : this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
21
42
  }
22
43
  /**
23
44
  * starts the instance
@@ -28,8 +49,6 @@ export class SmartAcme {
28
49
  async start() {
29
50
  this.privateKey =
30
51
  this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
31
- this.setChallenge = this.options.setChallenge;
32
- this.removeChallenge = this.options.removeChallenge;
33
52
  // CertMangaer
34
53
  this.certmanager = new SmartacmeCertManager(this, {
35
54
  mongoDescriptor: this.options.mongoDescriptor,
@@ -54,10 +73,63 @@ export class SmartAcme {
54
73
  termsOfServiceAgreed: true,
55
74
  contact: [`mailto:${this.options.accountEmail}`],
56
75
  });
76
+ // Setup graceful shutdown handlers
77
+ process.on('SIGINT', () => this.handleSignal('SIGINT'));
78
+ process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
57
79
  }
58
80
  async stop() {
59
81
  await this.certmanager.smartdataDb.close();
60
82
  }
83
+ /** Retry helper with exponential backoff */
84
+ async retry(operation, operationName = 'operation') {
85
+ let attempt = 0;
86
+ let delay = this.retryOptions.minTimeoutMs;
87
+ while (true) {
88
+ try {
89
+ return await operation();
90
+ }
91
+ catch (err) {
92
+ attempt++;
93
+ if (attempt > this.retryOptions.retries) {
94
+ await this.logger.log('error', `Operation ${operationName} failed after ${attempt} attempts`, err);
95
+ throw err;
96
+ }
97
+ await this.logger.log('warn', `Operation ${operationName} failed on attempt ${attempt}, retrying in ${delay}ms`, err);
98
+ await plugins.smartdelay.delayFor(delay);
99
+ delay = Math.min(delay * this.retryOptions.factor, this.retryOptions.maxTimeoutMs);
100
+ }
101
+ }
102
+ }
103
+ /** Clean up pending challenges and shut down */
104
+ async handleShutdown() {
105
+ for (const input of [...this.pendingChallenges]) {
106
+ const type = input.type;
107
+ const handler = this.challengeHandlers.find((h) => h.getSupportedTypes().includes(type));
108
+ if (handler) {
109
+ try {
110
+ await handler.cleanup(input);
111
+ await this.logger.log('info', `Removed pending ${type} challenge during shutdown`, input);
112
+ }
113
+ catch (err) {
114
+ await this.logger.log('error', `Failed to remove pending ${type} challenge during shutdown`, err);
115
+ }
116
+ }
117
+ else {
118
+ await this.logger.log('warn', `No handler for pending challenge type '${type}' during shutdown; skipping cleanup`, input);
119
+ }
120
+ }
121
+ this.pendingChallenges = [];
122
+ await this.stop();
123
+ }
124
+ /** Handle process signals for graceful shutdown */
125
+ handleSignal(sig) {
126
+ this.logger.log('info', `Received signal ${sig}, shutting down gracefully`);
127
+ this.handleShutdown()
128
+ .then(() => process.exit(0))
129
+ .catch((err) => {
130
+ this.logger.log('error', 'Error during shutdown', err).then(() => process.exit(1));
131
+ });
132
+ }
61
133
  /**
62
134
  * gets a certificate
63
135
  * it runs through the following steps
@@ -89,50 +161,76 @@ export class SmartAcme {
89
161
  }
90
162
  // lets make sure others get the same interest
91
163
  const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName);
92
- /* Place new order */
93
- const order = await this.client.createOrder({
164
+ /* Place new order with retry */
165
+ const order = await this.retry(() => this.client.createOrder({
94
166
  identifiers: [
95
167
  { type: 'dns', value: certDomainName },
96
168
  { type: 'dns', value: `*.${certDomainName}` },
97
169
  ],
98
- });
170
+ }), 'createOrder');
99
171
  /* Get authorizations and select challenges */
100
- const authorizations = await this.client.getAuthorizations(order);
172
+ const authorizations = await this.retry(() => this.client.getAuthorizations(order), 'getAuthorizations');
101
173
  for (const authz of authorizations) {
102
- console.log(authz);
103
- const fullHostName = `_acme-challenge.${authz.identifier.value}`;
104
- const dnsChallenge = authz.challenges.find((challengeArg) => {
105
- return challengeArg.type === 'dns-01';
106
- });
107
- // process.exit(1);
108
- const keyAuthorization = await this.client.getChallengeKeyAuthorization(dnsChallenge);
174
+ await this.logger.log('debug', 'Authorization received', authz);
175
+ // select a handler based on configured priority
176
+ let selectedHandler = null;
177
+ let selectedChallengeArg = null;
178
+ for (const type of this.challengePriority) {
179
+ const candidate = authz.challenges.find((c) => c.type === type);
180
+ if (!candidate)
181
+ continue;
182
+ const handler = this.challengeHandlers.find((h) => h.getSupportedTypes().includes(type));
183
+ if (handler) {
184
+ selectedHandler = { type, handler };
185
+ selectedChallengeArg = candidate;
186
+ break;
187
+ }
188
+ }
189
+ if (!selectedHandler) {
190
+ throw new Error(`No challenge handler for domain ${authz.identifier.value}: supported types [${this.challengePriority.join(',')}]`);
191
+ }
192
+ const { type, handler } = selectedHandler;
193
+ // build handler input with keyAuthorization
194
+ let input;
195
+ // retrieve keyAuthorization for challenge
196
+ const keyAuth = await this.client.getChallengeKeyAuthorization(selectedChallengeArg);
197
+ if (type === 'dns-01') {
198
+ input = { type, hostName: `_acme-challenge.${authz.identifier.value}`, challenge: keyAuth };
199
+ }
200
+ else if (type === 'http-01') {
201
+ // HTTP-01 requires serving token at webPath
202
+ input = {
203
+ type,
204
+ token: selectedChallengeArg.token,
205
+ keyAuthorization: keyAuth,
206
+ webPath: `/.well-known/acme-challenge/${selectedChallengeArg.token}`,
207
+ };
208
+ }
209
+ else {
210
+ // generic challenge input: include raw challenge properties
211
+ input = { type, keyAuthorization: keyAuth, ...selectedChallengeArg };
212
+ }
213
+ this.pendingChallenges.push(input);
109
214
  try {
110
- /* Satisfy challenge */
111
- await this.setChallenge({
112
- hostName: fullHostName,
113
- challenge: keyAuthorization,
114
- });
115
- await plugins.smartdelay.delayFor(30000);
116
- await this.smartdns.checkUntilAvailable(fullHostName, 'TXT', keyAuthorization, 100, 5000);
117
- console.log('Cool down an extra 60 second for region availability');
118
- await plugins.smartdelay.delayFor(60000);
119
- /* Verify that challenge is satisfied */
120
- await this.client.verifyChallenge(authz, dnsChallenge);
121
- /* Notify ACME provider that challenge is satisfied */
122
- await this.client.completeChallenge(dnsChallenge);
123
- /* Wait for ACME provider to respond with valid status */
124
- await this.client.waitForValidStatus(dnsChallenge);
215
+ await this.retry(() => handler.prepare(input), `${type}.prepare`);
216
+ if (handler.verify) {
217
+ await this.retry(() => handler.verify(input), `${type}.verify`);
218
+ }
219
+ else {
220
+ await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
221
+ }
222
+ await this.retry(() => this.client.completeChallenge(selectedChallengeArg), `${type}.completeChallenge`);
223
+ await this.retry(() => this.client.waitForValidStatus(selectedChallengeArg), `${type}.waitForValidStatus`);
125
224
  }
126
225
  finally {
127
- /* Clean up challenge response */
128
226
  try {
129
- await this.removeChallenge({
130
- hostName: fullHostName,
131
- challenge: keyAuthorization,
132
- });
227
+ await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
228
+ }
229
+ catch (err) {
230
+ await this.logger.log('error', `Error during ${type}.cleanup`, err);
133
231
  }
134
- catch (e) {
135
- console.log(e);
232
+ finally {
233
+ this.pendingChallenges = this.pendingChallenges.filter((c) => c !== input);
136
234
  }
137
235
  }
138
236
  }
@@ -141,8 +239,8 @@ export class SmartAcme {
141
239
  commonName: `*.${certDomainName}`,
142
240
  altNames: [certDomainName],
143
241
  });
144
- await this.client.finalizeOrder(order, csr);
145
- const cert = await this.client.getCertificate(order);
242
+ await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
243
+ const cert = await this.retry(() => this.client.getCertificate(order), 'getCertificate');
146
244
  /* Done */
147
245
  await this.certmanager.storeCertificate({
148
246
  id: plugins.smartunique.shortId(),
@@ -165,4 +263,4 @@ export class SmartAcme {
165
263
  return SmartacmeCert.getInstances({});
166
264
  }
167
265
  }
168
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhY21lLmNsYXNzZXMuc21hcnRhY21lLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc21hcnRhY21lLmNsYXNzZXMuc21hcnRhY21lLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sd0JBQXdCLENBQUM7QUFDbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQzVELE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQWNyRDs7Ozs7Ozs7O0dBU0c7QUFDSCxNQUFNLE9BQU8sU0FBUztJQW1CcEIsWUFBWSxVQUE2QjtRQWRqQyxhQUFRLEdBQUcsSUFBSSxPQUFPLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQWVuRCxJQUFJLENBQUMsT0FBTyxHQUFHLFVBQVUsQ0FBQztRQUMxQixJQUFJLENBQUMsTUFBTSxHQUFHLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLG1CQUFtQixDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQzFFLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNJLEtBQUssQ0FBQyxLQUFLO1FBQ2hCLElBQUksQ0FBQyxVQUFVO1lBQ2IsSUFBSSxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsSUFBSSxDQUFDLE1BQU0sT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQzdGLElBQUksQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxZQUFZLENBQUM7UUFDOUMsSUFBSSxDQUFDLGVBQWUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLGVBQWUsQ0FBQztRQUVwRCxjQUFj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
266
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhY21lLmNsYXNzZXMuc21hcnRhY21lLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc21hcnRhY21lLmNsYXNzZXMuc21hcnRhY21lLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sd0JBQXdCLENBQUM7QUFDbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQzVELE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQW1DckQ7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxPQUFPLFNBQVM7SUF3QnBCLFlBQVksVUFBNkI7UUFuQmpDLGFBQVEsR0FBRyxJQUFJLE9BQU8sQ0FBQyxjQUFjLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBWTNELHFEQUFxRDtRQUM3QyxzQkFBaUIsR0FBNEMsRUFBRSxDQUFDO1FBT3RFLElBQUksQ0FBQyxPQUFPLEdBQUcsVUFBVSxDQUFDO1FBQzFCLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsbUJBQW1CLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDeEUsK0NBQStDO1FBQy9DLElBQUksQ0FBQyxNQUFNLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDNUIsbUNBQW1DO1FBQ25DLElBQUksQ0FBQyxZQUFZLEdBQUc7WUFDbEIsT0FBTyxFQUFFLFVBQVUsQ0FBQyxZQUFZLEVBQUUsT0FBTyxJQUFJLENBQUM7WUFDOUMsTUFBTSxFQUFFLFVBQVUsQ0FBQyxZQUFZLEVBQUUsTUFBTSxJQUFJLENBQUM7WUFDNUMsWUFBWSxFQUFFLFVBQVUsQ0FBQyxZQUFZLEVBQUUsWUFBWSxJQUFJLElBQUk7WUFDM0QsWUFBWSxF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
@@ -3,15 +3,17 @@ export { typedserver };
3
3
  import * as lik from '@push.rocks/lik';
4
4
  import * as smartdata from '@push.rocks/smartdata';
5
5
  import * as smartdelay from '@push.rocks/smartdelay';
6
- import * as smartdns from '@push.rocks/smartdns';
6
+ import * as smartdnsClient from '@push.rocks/smartdns/client';
7
7
  import * as smartlog from '@push.rocks/smartlog';
8
8
  import * as smartpromise from '@push.rocks/smartpromise';
9
9
  import * as smartrequest from '@push.rocks/smartrequest';
10
10
  import * as smartunique from '@push.rocks/smartunique';
11
11
  import * as smartstring from '@push.rocks/smartstring';
12
12
  import * as smarttime from '@push.rocks/smarttime';
13
- export { lik, smartdata, smartdelay, smartdns, smartlog, smartpromise, smartrequest, smartunique, smartstring, smarttime, };
13
+ export { lik, smartdata, smartdelay, smartdnsClient, smartlog, smartpromise, smartrequest, smartunique, smartstring, smarttime, };
14
14
  import * as tsclass from '@tsclass/tsclass';
15
15
  export { tsclass };
16
16
  import * as acme from 'acme-client';
17
17
  export { acme };
18
+ import * as handlers from './handlers/index.js';
19
+ export { handlers };
@@ -5,18 +5,21 @@ export { typedserver };
5
5
  import * as lik from '@push.rocks/lik';
6
6
  import * as smartdata from '@push.rocks/smartdata';
7
7
  import * as smartdelay from '@push.rocks/smartdelay';
8
- import * as smartdns from '@push.rocks/smartdns';
8
+ import * as smartdnsClient from '@push.rocks/smartdns/client';
9
9
  import * as smartlog from '@push.rocks/smartlog';
10
10
  import * as smartpromise from '@push.rocks/smartpromise';
11
11
  import * as smartrequest from '@push.rocks/smartrequest';
12
12
  import * as smartunique from '@push.rocks/smartunique';
13
13
  import * as smartstring from '@push.rocks/smartstring';
14
14
  import * as smarttime from '@push.rocks/smarttime';
15
- export { lik, smartdata, smartdelay, smartdns, smartlog, smartpromise, smartrequest, smartunique, smartstring, smarttime, };
15
+ export { lik, smartdata, smartdelay, smartdnsClient, smartlog, smartpromise, smartrequest, smartunique, smartstring, smarttime, };
16
16
  // @tsclass scope
17
17
  import * as tsclass from '@tsclass/tsclass';
18
18
  export { tsclass };
19
19
  // third party scope
20
20
  import * as acme from 'acme-client';
21
21
  export { acme };
22
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhY21lLnBsdWdpbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydGFjbWUucGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxtQkFBbUI7QUFDbkIsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUV2RCxPQUFPLEVBQUUsV0FBVyxFQUFFLENBQUM7QUFFdkIsbUJBQW1CO0FBQ25CLE9BQU8sS0FBSyxHQUFHLE1BQU0saUJBQWlCLENBQUM7QUFDdkMsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxRQUFRLE1BQU0sc0JBQXNCLENBQUM7QUFDakQsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssWUFBWSxNQUFNLDBCQUEwQixDQUFDO0FBQ3pELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssV0FBVyxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxTQUFTLE1BQU0sdUJBQXVCLENBQUM7QUFFbkQsT0FBTyxFQUNMLEdBQUcsRUFDSCxTQUFTLEVBQ1QsVUFBVSxFQUNWLFFBQVEsRUFDUixRQUFRLEVBQ1IsWUFBWSxFQUNaLFlBQVksRUFDWixXQUFXLEVBQ1gsV0FBVyxFQUNYLFNBQVMsR0FDVixDQUFDO0FBRUYsaUJBQWlCO0FBQ2pCLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFFNUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxDQUFDO0FBRW5CLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssSUFBSSxNQUFNLGFBQWEsQ0FBQztBQUVwQyxPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUMifQ==
22
+ // local handlers for challenge types
23
+ import * as handlers from './handlers/index.js';
24
+ export { handlers };
25
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhY21lLnBsdWdpbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydGFjbWUucGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxtQkFBbUI7QUFDbkIsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUV2RCxPQUFPLEVBQUUsV0FBVyxFQUFFLENBQUM7QUFFdkIsbUJBQW1CO0FBQ25CLE9BQU8sS0FBSyxHQUFHLE1BQU0saUJBQWlCLENBQUM7QUFDdkMsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxjQUFjLE1BQU0sNkJBQTZCLENBQUM7QUFDOUQsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssWUFBWSxNQUFNLDBCQUEwQixDQUFDO0FBQ3pELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssV0FBVyxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxTQUFTLE1BQU0sdUJBQXVCLENBQUM7QUFFbkQsT0FBTyxFQUNMLEdBQUcsRUFDSCxTQUFTLEVBQ1QsVUFBVSxFQUNWLGNBQWMsRUFDZCxRQUFRLEVBQ1IsWUFBWSxFQUNaLFlBQVksRUFDWixXQUFXLEVBQ1gsV0FBVyxFQUNYLFNBQVMsR0FDVixDQUFDO0FBRUYsaUJBQWlCO0FBQ2pCLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFFNUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxDQUFDO0FBRW5CLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssSUFBSSxNQUFNLGFBQWEsQ0FBQztBQUVwQyxPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUM7QUFDaEIscUNBQXFDO0FBQ3JDLE9BQU8sS0FBSyxRQUFRLE1BQU0scUJBQXFCLENBQUM7QUFDaEQsT0FBTyxFQUFFLFFBQVEsRUFBRSxDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartacme",
3
- "version": "5.0.0",
3
+ "version": "6.0.1",
4
4
  "private": false,
5
5
  "description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
6
6
  "main": "dist_ts/index.js",
@@ -27,35 +27,35 @@
27
27
  "certificate renewal",
28
28
  "wildcard certificates"
29
29
  ],
30
- "author": "Lossless GmbH",
30
+ "author": "Task Venture Capital GmbH",
31
31
  "license": "MIT",
32
32
  "bugs": {
33
- "url": "https://gitlab.com/umbrellazone/smartacme/issues"
33
+ "url": "https://code.foss.global/push.rocks/smartacme/issues"
34
34
  },
35
- "homepage": "https://code.foss.global/push.rocks/smartacme",
35
+ "homepage": "https://code.foss.global/push.rocks/smartacme#readme",
36
36
  "dependencies": {
37
- "@api.global/typedserver": "^3.0.50",
38
- "@push.rocks/lik": "^6.0.15",
39
- "@push.rocks/smartdata": "^5.2.4",
37
+ "@api.global/typedserver": "^3.0.74",
38
+ "@push.rocks/lik": "^6.2.2",
39
+ "@push.rocks/smartdata": "^5.15.1",
40
40
  "@push.rocks/smartdelay": "^3.0.5",
41
- "@push.rocks/smartdns": "^5.0.2",
41
+ "@push.rocks/smartdns": "^6.2.2",
42
42
  "@push.rocks/smartlog": "^3.0.7",
43
- "@push.rocks/smartpromise": "^4.0.3",
44
- "@push.rocks/smartrequest": "^2.0.22",
43
+ "@push.rocks/smartpromise": "^4.2.3",
44
+ "@push.rocks/smartrequest": "^2.1.0",
45
45
  "@push.rocks/smartstring": "^4.0.15",
46
- "@push.rocks/smarttime": "^4.0.6",
46
+ "@push.rocks/smarttime": "^4.1.1",
47
47
  "@push.rocks/smartunique": "^3.0.9",
48
- "@tsclass/tsclass": "^4.0.58",
49
- "acme-client": "^4.2.5"
48
+ "@tsclass/tsclass": "^9.0.0",
49
+ "acme-client": "^5.4.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@apiclient.xyz/cloudflare": "^6.0.3",
53
- "@git.zone/tsbuild": "^2.1.80",
54
- "@git.zone/tsrun": "^1.2.44",
55
- "@git.zone/tstest": "^1.0.90",
56
- "@push.rocks/qenv": "^6.0.5",
57
- "@push.rocks/tapbundle": "^5.0.23",
58
- "@types/node": "^20.14.2"
52
+ "@apiclient.xyz/cloudflare": "^6.3.2",
53
+ "@git.zone/tsbuild": "^2.3.2",
54
+ "@git.zone/tsrun": "^1.3.3",
55
+ "@git.zone/tstest": "^1.0.96",
56
+ "@push.rocks/qenv": "^6.1.0",
57
+ "@push.rocks/tapbundle": "^5.6.3",
58
+ "@types/node": "^22.15.2"
59
59
  },
60
60
  "files": [
61
61
  "ts/**/*",
package/readme.md CHANGED
@@ -1,4 +1,3 @@
1
- ```markdown
2
1
  # @push.rocks/smartacme
3
2
 
4
3
  A TypeScript-based ACME client with an easy yet powerful interface for LetsEncrypt certificate management.
@@ -40,35 +39,40 @@ Ensure your project includes the necessary TypeScript configuration and dependen
40
39
 
41
40
  ### Creating a SmartAcme Instance
42
41
 
43
- Start by importing the `SmartAcme` class from the `@push.rocks/smartacme` package. You'll also need to import or define interfaces for your setup options:
42
+ Start by importing the `SmartAcme` class and any built-in handlers you plan to use. For example, to use DNS-01 via Cloudflare:
44
43
 
45
44
  ```typescript
46
45
  import { SmartAcme } from '@push.rocks/smartacme';
46
+ import * as cloudflare from '@apiclient.xyz/cloudflare';
47
+ import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
48
+
49
+ // Create a Cloudflare account client with your API token
50
+ const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
47
51
 
52
+ // Instantiate SmartAcme with one or more ACME challenge handlers
48
53
  const smartAcmeInstance = new SmartAcme({
49
- accountEmail: 'youremail@example.com', // Email used for Let's Encrypt registration and recovery
50
- accountPrivateKey: null, // Private key for the account (optional, if not provided it will be generated)
54
+ accountEmail: 'youremail@example.com',
51
55
  mongoDescriptor: {
52
56
  mongoDbUrl: 'mongodb://yourmongoURL',
53
57
  mongoDbName: 'yourDbName',
54
58
  mongoDbPass: 'yourDbPassword',
55
59
  },
56
- removeChallenge: async (dnsChallenge) => {
57
- // Implement logic here to remove DNS challenge records
58
- },
59
- setChallenge: async (dnsChallenge) => {
60
- // Implement logic here to create DNS challenge records
61
- },
62
- environment: 'integration', // Use 'production' for actual certificates
60
+ environment: 'integration', // 'production' to request real certificates
61
+ retryOptions: {}, // optional retry/backoff settings
62
+ challengeHandlers: [
63
+ new Dns01Handler(cfAccount),
64
+ // you can add more handlers, e.g. Http01Handler
65
+ ],
66
+ challengePriority: ['dns-01'], // optional ordering of challenge types
63
67
  });
64
68
  ```
65
69
 
66
70
  ### Initializing SmartAcme
67
71
 
68
- Before proceeding to request certificates, initialize your SmartAcme instance:
72
+ Before proceeding to request certificates, start your SmartAcme instance:
69
73
 
70
74
  ```typescript
71
- await smartAcmeInstance.init();
75
+ await smartAcmeInstance.start();
72
76
  ```
73
77
 
74
78
  ### Obtaining a Certificate for a Domain
@@ -83,34 +87,7 @@ console.log('Certificate:', myCert);
83
87
 
84
88
  ### Automating DNS Challenges
85
89
 
86
- Part of the ACME protocol involves responding to DNS challenges issued by the certificate authority to prove control over a domain. Implement the `setChallenge` and `removeChallenge` functions in your SmartAcme configuration to automate this process. These functions receive a `dnsChallenge` argument containing details needed to create or remove the necessary DNS records.
87
-
88
- ```typescript
89
- import * as cloudflare from '@apiclient.xyz/cloudflare';
90
- import { Qenv } from '@push.rocks/qenv';
91
-
92
- const testQenv = new Qenv('./', './.nogit/');
93
- const testCloudflare = new cloudflare.CloudflareAccount(testQenv.getEnvVarOnDemand('CF_TOKEN'));
94
-
95
- const smartAcmeInstance = new SmartAcme({
96
- accountEmail: 'domains@example.com',
97
- accountPrivateKey: null,
98
- mongoDescriptor: {
99
- mongoDbName: testQenv.getEnvVarRequired('MONGODB_DATABASE'),
100
- mongoDbPass: testQenv.getEnvVarRequired('MONGODB_PASSWORD'),
101
- mongoDbUrl: testQenv.getEnvVarRequired('MONGODB_URL'),
102
- },
103
- removeChallenge: async (dnsChallenge) => {
104
- testCloudflare.convenience.acmeRemoveDnsChallenge(dnsChallenge);
105
- },
106
- setChallenge: async (dnsChallenge) => {
107
- testCloudflare.convenience.acmeSetDnsChallenge(dnsChallenge);
108
- },
109
- environment: 'integration',
110
- });
111
-
112
- await smartAcmeInstance.init();
113
- ```
90
+ 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.
114
91
 
115
92
  ### Managing Certificates
116
93
 
@@ -130,7 +107,7 @@ When creating an instance of `SmartAcme`, you can specify an `environment` optio
130
107
 
131
108
  ### Complete Example
132
109
 
133
- Below is a complete example demonstrating how to use `@push.rocks/smartacme` to obtain and manage an ACME certificate with Let's Encrypt:
110
+ 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:
134
111
 
135
112
  ```typescript
136
113
  import { SmartAcme } from '@push.rocks/smartacme';
@@ -143,22 +120,16 @@ const cloudflareAccount = new cloudflare.CloudflareAccount(qenv.getEnvVarOnDeman
143
120
  async function main() {
144
121
  const smartAcmeInstance = new SmartAcme({
145
122
  accountEmail: 'youremail@example.com',
146
- accountPrivateKey: null,
147
123
  mongoDescriptor: {
148
124
  mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
149
125
  mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
150
126
  mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
151
127
  },
152
- setChallenge: async (dnsChallenge) => {
153
- await cloudflareAccount.convenience.acmeSetDnsChallenge(dnsChallenge);
154
- },
155
- removeChallenge: async (dnsChallenge) => {
156
- await cloudflareAccount.convenience.acmeRemoveDnsChallenge(dnsChallenge);
157
- },
158
128
  environment: 'integration',
129
+ challengeHandlers: [ new Dns01Handler(cloudflareAccount) ],
159
130
  });
160
131
 
161
- await smartAcmeInstance.init();
132
+ await smartAcmeInstance.start();
162
133
 
163
134
  const myDomain = 'example.com';
164
135
  const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain);
@@ -265,11 +236,10 @@ tap.start();
265
236
  This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`.
266
237
 
267
238
  ---
268
- ```
269
239
 
270
240
  ## License and Legal Information
271
241
 
272
- 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](license) file within this repository.
242
+ 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](license) file within this repository.
273
243
 
274
244
  **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.
275
245
 
@@ -279,7 +249,7 @@ This project is owned and maintained by Task Venture Capital GmbH. The names and
279
249
 
280
250
  ### Company Information
281
251
 
282
- Task Venture Capital GmbH
252
+ Task Venture Capital GmbH
283
253
  Registered at District court Bremen HRB 35230 HB, Germany
284
254
 
285
255
  For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
@@ -1,8 +1,8 @@
1
1
  /**
2
- * autocreated commitinfo by @pushrocks/commitinfo
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartacme',
6
- version: '5.0.0',
6
+ version: '6.0.1',
7
7
  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
8
8
  }
@@ -0,0 +1,40 @@
1
+ import * as plugins from '../smartacme.plugins.js';
2
+ import type { IChallengeHandler } from './IChallengeHandler.js';
3
+
4
+ /**
5
+ * DNS-01 challenge handler using CloudflareAccount and Smartdns.
6
+ */
7
+ export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.IDnsChallenge> {
8
+ private cf: any;
9
+ private smartdns: plugins.smartdnsClient.Smartdns;
10
+
11
+ constructor(
12
+ cloudflareAccount: any,
13
+ smartdnsInstance?: plugins.smartdnsClient.Smartdns,
14
+ ) {
15
+ this.cf = cloudflareAccount;
16
+ this.smartdns = smartdnsInstance ?? new plugins.smartdnsClient.Smartdns({});
17
+ }
18
+
19
+ public getSupportedTypes(): string[] {
20
+ return ['dns-01'];
21
+ }
22
+
23
+ public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
24
+ // set DNS TXT record
25
+ await this.cf.convenience.acmeSetDnsChallenge(ch);
26
+ // wait for DNS propagation
27
+ await this.smartdns.checkUntilAvailable(
28
+ ch.hostName,
29
+ 'TXT',
30
+ ch.challenge,
31
+ 100,
32
+ 5000,
33
+ );
34
+ }
35
+
36
+ public async cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
37
+ // remove DNS TXT record
38
+ await this.cf.convenience.acmeRemoveDnsChallenge(ch);
39
+ }
40
+ }
@@ -0,0 +1,54 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import type { IChallengeHandler } from './IChallengeHandler.js';
4
+
5
+ /**
6
+ * HTTP-01 ACME challenge handler using file-system webroot.
7
+ * Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/.
8
+ */
9
+ export interface Http01HandlerOptions {
10
+ /**
11
+ * Directory that serves HTTP requests for /.well-known/acme-challenge
12
+ */
13
+ webroot: string;
14
+ }
15
+
16
+ export class Http01Handler implements IChallengeHandler<{
17
+ type: string;
18
+ token: string;
19
+ keyAuthorization: string;
20
+ webPath: string;
21
+ }> {
22
+ private webroot: string;
23
+
24
+ constructor(options: Http01HandlerOptions) {
25
+ this.webroot = options.webroot;
26
+ }
27
+
28
+ public getSupportedTypes(): string[] {
29
+ return ['http-01'];
30
+ }
31
+
32
+ public async prepare(ch: { token: string; keyAuthorization: string; webPath: string }): Promise<void> {
33
+ const relWebPath = ch.webPath.replace(/^\/+/, '');
34
+ const filePath = path.join(this.webroot, relWebPath);
35
+ const dir = path.dirname(filePath);
36
+ await fs.mkdir(dir, { recursive: true });
37
+ await fs.writeFile(filePath, ch.keyAuthorization, 'utf8');
38
+ }
39
+
40
+ public async verify(ch: { webPath: string; keyAuthorization: string }): Promise<void> {
41
+ // Optional: implement HTTP polling if desired
42
+ return;
43
+ }
44
+
45
+ public async cleanup(ch: { token: string; webPath: string }): Promise<void> {
46
+ const relWebPath = ch.webPath.replace(/^\/+/, '');
47
+ const filePath = path.join(this.webroot, relWebPath);
48
+ try {
49
+ await fs.unlink(filePath);
50
+ } catch {
51
+ // ignore missing file
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pluggable interface for ACME challenge handlers.
3
+ * Supports DNS-01, HTTP-01, TLS-ALPN-01, or custom challenge types.
4
+ */
5
+ export interface IChallengeHandler<T> {
6
+ /**
7
+ * ACME challenge types this handler supports (e.g. ['dns-01']).
8
+ */
9
+ getSupportedTypes(): string[];
10
+ /**
11
+ * Prepare the challenge: set DNS record, start HTTP/TLS server, etc.
12
+ */
13
+ prepare(ch: T): Promise<void>;
14
+ /**
15
+ * Optional extra verify step (HTTP GET, ALPN handshake).
16
+ */
17
+ verify?(ch: T): Promise<void>;
18
+ /**
19
+ * Clean up resources: remove DNS record, stop server.
20
+ */
21
+ cleanup(ch: T): Promise<void>;
22
+ }
@@ -0,0 +1,4 @@
1
+ export type { IChallengeHandler } from './IChallengeHandler.js';
2
+ // Removed legacy handler adapter
3
+ export { Dns01Handler } from './Dns01Handler.js';
4
+ export { Http01Handler } from './Http01Handler.js';
package/ts/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from './smartacme.classes.smartacme.js';
2
- export { SmartacmeCert as Cert } from './smartacme.classes.cert.js';
2
+ export { SmartacmeCert as Cert } from './smartacme.classes.cert.js';
@@ -22,7 +22,7 @@ export class SmartacmeCertManager {
22
22
  smartAcmeArg: SmartAcme,
23
23
  optionsArg: {
24
24
  mongoDescriptor: plugins.smartdata.IMongoDescriptor;
25
- }
25
+ },
26
26
  ) {
27
27
  this.mongoDescriptor = optionsArg.mongoDescriptor;
28
28
  }
@@ -11,9 +11,30 @@ export interface ISmartAcmeOptions {
11
11
  accountPrivateKey?: string;
12
12
  accountEmail: string;
13
13
  mongoDescriptor: plugins.smartdata.IMongoDescriptor;
14
- setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
15
- removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
14
+ // Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
16
15
  environment: 'production' | 'integration';
16
+ /**
17
+ * Optional retry/backoff configuration for transient failures
18
+ */
19
+ retryOptions?: {
20
+ /** number of retry attempts */
21
+ retries?: number;
22
+ /** backoff multiplier */
23
+ factor?: number;
24
+ /** initial delay in milliseconds */
25
+ minTimeoutMs?: number;
26
+ /** maximum delay cap in milliseconds */
27
+ maxTimeoutMs?: number;
28
+ };
29
+ /**
30
+ * Pluggable ACME challenge handlers (DNS-01, HTTP-01, TLS-ALPN-01, etc.)
31
+ */
32
+ challengeHandlers?: plugins.handlers.IChallengeHandler<any>[];
33
+ /**
34
+ * Order of challenge types to try (e.g. ['http-01','dns-01']).
35
+ * Defaults to ['dns-01'] or first supported type from handlers.
36
+ */
37
+ challengePriority?: string[];
17
38
  }
18
39
 
19
40
  /**
@@ -30,24 +51,50 @@ export class SmartAcme {
30
51
  private options: ISmartAcmeOptions;
31
52
 
32
53
  // the acme client
33
- private client: any;
34
- private smartdns = new plugins.smartdns.Smartdns({});
54
+ private client: plugins.acme.Client;
55
+ private smartdns = new plugins.smartdnsClient.Smartdns({});
35
56
  public logger: plugins.smartlog.Smartlog;
36
57
 
37
58
  // the account private key
38
59
  private privateKey: string;
39
60
 
40
- // challenge fullfillment
41
- private setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
42
- private removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
43
61
 
44
62
  // certmanager
45
63
  private certmanager: SmartacmeCertManager;
46
64
  private certmatcher: SmartacmeCertMatcher;
65
+ // retry/backoff configuration (resolved with defaults)
66
+ private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
67
+ // track pending DNS challenges for graceful shutdown
68
+ private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = [];
69
+ // configured pluggable ACME challenge handlers
70
+ private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
71
+ // priority order of challenge types
72
+ private challengePriority: string[];
47
73
 
48
74
  constructor(optionsArg: ISmartAcmeOptions) {
49
75
  this.options = optionsArg;
50
76
  this.logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
77
+ // enable console output for structured logging
78
+ this.logger.enableConsole();
79
+ // initialize retry/backoff options
80
+ this.retryOptions = {
81
+ retries: optionsArg.retryOptions?.retries ?? 3,
82
+ factor: optionsArg.retryOptions?.factor ?? 2,
83
+ minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
84
+ maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
85
+ };
86
+ // initialize challenge handlers (must provide at least one)
87
+ if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
88
+ throw new Error(
89
+ 'You must provide at least one ACME challenge handler via options.challengeHandlers',
90
+ );
91
+ }
92
+ this.challengeHandlers = optionsArg.challengeHandlers;
93
+ // initialize challenge priority
94
+ this.challengePriority =
95
+ optionsArg.challengePriority && optionsArg.challengePriority.length > 0
96
+ ? optionsArg.challengePriority
97
+ : this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
51
98
  }
52
99
 
53
100
  /**
@@ -59,8 +106,6 @@ export class SmartAcme {
59
106
  public async start() {
60
107
  this.privateKey =
61
108
  this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
62
- this.setChallenge = this.options.setChallenge;
63
- this.removeChallenge = this.options.removeChallenge;
64
109
 
65
110
  // CertMangaer
66
111
  this.certmanager = new SmartacmeCertManager(this, {
@@ -88,11 +133,65 @@ export class SmartAcme {
88
133
  termsOfServiceAgreed: true,
89
134
  contact: [`mailto:${this.options.accountEmail}`],
90
135
  });
136
+ // Setup graceful shutdown handlers
137
+ process.on('SIGINT', () => this.handleSignal('SIGINT'));
138
+ process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
91
139
  }
92
140
 
93
- public async stop() {
94
- await this.certmanager.smartdataDb.close();
95
- }
141
+ public async stop() {
142
+ await this.certmanager.smartdataDb.close();
143
+ }
144
+ /** Retry helper with exponential backoff */
145
+ private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
146
+ let attempt = 0;
147
+ let delay = this.retryOptions.minTimeoutMs;
148
+ while (true) {
149
+ try {
150
+ return await operation();
151
+ } catch (err) {
152
+ attempt++;
153
+ if (attempt > this.retryOptions.retries) {
154
+ await this.logger.log('error', `Operation ${operationName} failed after ${attempt} attempts`, err);
155
+ throw err;
156
+ }
157
+ await this.logger.log('warn', `Operation ${operationName} failed on attempt ${attempt}, retrying in ${delay}ms`, err);
158
+ await plugins.smartdelay.delayFor(delay);
159
+ delay = Math.min(delay * this.retryOptions.factor, this.retryOptions.maxTimeoutMs);
160
+ }
161
+ }
162
+ }
163
+ /** Clean up pending challenges and shut down */
164
+ private async handleShutdown(): Promise<void> {
165
+ for (const input of [...this.pendingChallenges]) {
166
+ const type: string = (input as any).type;
167
+ const handler = this.challengeHandlers.find((h) => h.getSupportedTypes().includes(type));
168
+ if (handler) {
169
+ try {
170
+ await handler.cleanup(input);
171
+ await this.logger.log('info', `Removed pending ${type} challenge during shutdown`, input);
172
+ } catch (err) {
173
+ await this.logger.log('error', `Failed to remove pending ${type} challenge during shutdown`, err);
174
+ }
175
+ } else {
176
+ await this.logger.log(
177
+ 'warn',
178
+ `No handler for pending challenge type '${type}' during shutdown; skipping cleanup`,
179
+ input,
180
+ );
181
+ }
182
+ }
183
+ this.pendingChallenges = [];
184
+ await this.stop();
185
+ }
186
+ /** Handle process signals for graceful shutdown */
187
+ private handleSignal(sig: string): void {
188
+ this.logger.log('info', `Received signal ${sig}, shutting down gracefully`);
189
+ this.handleShutdown()
190
+ .then(() => process.exit(0))
191
+ .catch((err) => {
192
+ this.logger.log('error', 'Error during shutdown', err).then(() => process.exit(1));
193
+ });
194
+ }
96
195
 
97
196
  /**
98
197
  * gets a certificate
@@ -128,54 +227,71 @@ export class SmartAcme {
128
227
  // lets make sure others get the same interest
129
228
  const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName);
130
229
 
131
- /* Place new order */
132
- const order = await this.client.createOrder({
230
+ /* Place new order with retry */
231
+ const order = await this.retry(() => this.client.createOrder({
133
232
  identifiers: [
134
233
  { type: 'dns', value: certDomainName },
135
234
  { type: 'dns', value: `*.${certDomainName}` },
136
235
  ],
137
- });
236
+ }), 'createOrder');
138
237
 
139
238
  /* Get authorizations and select challenges */
140
- const authorizations = await this.client.getAuthorizations(order);
239
+ const authorizations = await this.retry(() => this.client.getAuthorizations(order), 'getAuthorizations');
141
240
 
142
241
  for (const authz of authorizations) {
143
- console.log(authz);
144
- const fullHostName: string = `_acme-challenge.${authz.identifier.value}`;
145
- const dnsChallenge: string = authz.challenges.find((challengeArg) => {
146
- return challengeArg.type === 'dns-01';
147
- });
148
- // process.exit(1);
149
- const keyAuthorization: string = await this.client.getChallengeKeyAuthorization(dnsChallenge);
150
-
242
+ await this.logger.log('debug', 'Authorization received', authz);
243
+ // select a handler based on configured priority
244
+ let selectedHandler: { type: string; handler: plugins.handlers.IChallengeHandler<any> } | null = null;
245
+ let selectedChallengeArg: any = null;
246
+ for (const type of this.challengePriority) {
247
+ const candidate = authz.challenges.find((c: any) => c.type === type);
248
+ if (!candidate) continue;
249
+ const handler = this.challengeHandlers.find((h) => h.getSupportedTypes().includes(type));
250
+ if (handler) {
251
+ selectedHandler = { type, handler };
252
+ selectedChallengeArg = candidate;
253
+ break;
254
+ }
255
+ }
256
+ if (!selectedHandler) {
257
+ throw new Error(`No challenge handler for domain ${authz.identifier.value}: supported types [${this.challengePriority.join(',')}]`);
258
+ }
259
+ const { type, handler } = selectedHandler;
260
+ // build handler input with keyAuthorization
261
+ let input: any;
262
+ // retrieve keyAuthorization for challenge
263
+ const keyAuth = await this.client.getChallengeKeyAuthorization(selectedChallengeArg);
264
+ if (type === 'dns-01') {
265
+ input = { type, hostName: `_acme-challenge.${authz.identifier.value}`, challenge: keyAuth };
266
+ } else if (type === 'http-01') {
267
+ // HTTP-01 requires serving token at webPath
268
+ input = {
269
+ type,
270
+ token: (selectedChallengeArg as any).token,
271
+ keyAuthorization: keyAuth,
272
+ webPath: `/.well-known/acme-challenge/${(selectedChallengeArg as any).token}`,
273
+ };
274
+ } else {
275
+ // generic challenge input: include raw challenge properties
276
+ input = { type, keyAuthorization: keyAuth, ...selectedChallengeArg };
277
+ }
278
+ this.pendingChallenges.push(input);
151
279
  try {
152
- /* Satisfy challenge */
153
- await this.setChallenge({
154
- hostName: fullHostName,
155
- challenge: keyAuthorization,
156
- });
157
- await plugins.smartdelay.delayFor(30000);
158
- await this.smartdns.checkUntilAvailable(fullHostName, 'TXT', keyAuthorization, 100, 5000);
159
- console.log('Cool down an extra 60 second for region availability');
160
- await plugins.smartdelay.delayFor(60000);
161
-
162
- /* Verify that challenge is satisfied */
163
- await this.client.verifyChallenge(authz, dnsChallenge);
164
-
165
- /* Notify ACME provider that challenge is satisfied */
166
- await this.client.completeChallenge(dnsChallenge);
167
-
168
- /* Wait for ACME provider to respond with valid status */
169
- await this.client.waitForValidStatus(dnsChallenge);
280
+ await this.retry(() => handler.prepare(input), `${type}.prepare`);
281
+ if (handler.verify) {
282
+ await this.retry(() => handler.verify!(input), `${type}.verify`);
283
+ } else {
284
+ await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
285
+ }
286
+ await this.retry(() => this.client.completeChallenge(selectedChallengeArg), `${type}.completeChallenge`);
287
+ await this.retry(() => this.client.waitForValidStatus(selectedChallengeArg), `${type}.waitForValidStatus`);
170
288
  } finally {
171
- /* Clean up challenge response */
172
289
  try {
173
- await this.removeChallenge({
174
- hostName: fullHostName,
175
- challenge: keyAuthorization,
176
- });
177
- } catch (e) {
178
- console.log(e);
290
+ await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
291
+ } catch (err) {
292
+ await this.logger.log('error', `Error during ${type}.cleanup`, err);
293
+ } finally {
294
+ this.pendingChallenges = this.pendingChallenges.filter((c) => c !== input);
179
295
  }
180
296
  }
181
297
  }
@@ -186,8 +302,8 @@ export class SmartAcme {
186
302
  altNames: [certDomainName],
187
303
  });
188
304
 
189
- await this.client.finalizeOrder(order, csr);
190
- const cert = await this.client.getCertificate(order);
305
+ await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
306
+ const cert = await this.retry(() => this.client.getCertificate(order), 'getCertificate');
191
307
 
192
308
  /* Done */
193
309
 
@@ -7,7 +7,7 @@ export { typedserver };
7
7
  import * as lik from '@push.rocks/lik';
8
8
  import * as smartdata from '@push.rocks/smartdata';
9
9
  import * as smartdelay from '@push.rocks/smartdelay';
10
- import * as smartdns from '@push.rocks/smartdns';
10
+ import * as smartdnsClient from '@push.rocks/smartdns/client';
11
11
  import * as smartlog from '@push.rocks/smartlog';
12
12
  import * as smartpromise from '@push.rocks/smartpromise';
13
13
  import * as smartrequest from '@push.rocks/smartrequest';
@@ -19,7 +19,7 @@ export {
19
19
  lik,
20
20
  smartdata,
21
21
  smartdelay,
22
- smartdns,
22
+ smartdnsClient,
23
23
  smartlog,
24
24
  smartpromise,
25
25
  smartrequest,
@@ -37,3 +37,6 @@ export { tsclass };
37
37
  import * as acme from 'acme-client';
38
38
 
39
39
  export { acme };
40
+ // local handlers for challenge types
41
+ import * as handlers from './handlers/index.js';
42
+ export { handlers };