@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { AcmeCrypto } from './acme.classes.crypto.js';
|
|
2
|
+
import { AcmeError } from './acme.classes.error.js';
|
|
3
|
+
import type { AcmeHttpClient } from './acme.classes.http-client.js';
|
|
4
|
+
import type {
|
|
5
|
+
IAcmeAuthorization,
|
|
6
|
+
IAcmeIdentifier,
|
|
7
|
+
IAcmeOrder,
|
|
8
|
+
} from './acme.interfaces.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ACME order lifecycle management.
|
|
12
|
+
* Handles order creation, authorization retrieval, finalization, and certificate download.
|
|
13
|
+
*/
|
|
14
|
+
export class AcmeOrderManager {
|
|
15
|
+
private httpClient: AcmeHttpClient;
|
|
16
|
+
|
|
17
|
+
constructor(httpClient: AcmeHttpClient) {
|
|
18
|
+
this.httpClient = httpClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new ACME order for the given identifiers
|
|
23
|
+
*/
|
|
24
|
+
async create(opts: { identifiers: IAcmeIdentifier[] }): Promise<IAcmeOrder> {
|
|
25
|
+
const dir = await this.httpClient.getDirectory();
|
|
26
|
+
const response = await this.httpClient.signedRequest(dir.newOrder, {
|
|
27
|
+
identifiers: opts.identifiers,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const order = response.data as IAcmeOrder;
|
|
31
|
+
// Capture order URL from Location header
|
|
32
|
+
order.url = response.headers['location'] || '';
|
|
33
|
+
return order;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Retrieve all authorizations for an order (POST-as-GET each authorization URL)
|
|
38
|
+
*/
|
|
39
|
+
async getAuthorizations(order: IAcmeOrder): Promise<IAcmeAuthorization[]> {
|
|
40
|
+
const authorizations: IAcmeAuthorization[] = [];
|
|
41
|
+
for (const authzUrl of order.authorizations) {
|
|
42
|
+
const response = await this.httpClient.signedRequest(authzUrl, null);
|
|
43
|
+
authorizations.push(response.data as IAcmeAuthorization);
|
|
44
|
+
}
|
|
45
|
+
return authorizations;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Finalize an order by submitting the CSR.
|
|
50
|
+
* Waits for the order to reach 'valid' status.
|
|
51
|
+
* Mutates the order object with updated status and certificate URL.
|
|
52
|
+
*/
|
|
53
|
+
async finalize(order: IAcmeOrder, csrPem: string): Promise<void> {
|
|
54
|
+
// Convert PEM CSR to base64url DER for ACME
|
|
55
|
+
const csrDer = AcmeCrypto.pemToBuffer(csrPem);
|
|
56
|
+
const csrB64url = csrDer.toString('base64url');
|
|
57
|
+
|
|
58
|
+
const response = await this.httpClient.signedRequest(order.finalize, { csr: csrB64url });
|
|
59
|
+
|
|
60
|
+
// Update order with response data
|
|
61
|
+
const updatedOrder = response.data;
|
|
62
|
+
order.status = updatedOrder.status;
|
|
63
|
+
if (updatedOrder.certificate) {
|
|
64
|
+
order.certificate = updatedOrder.certificate;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If not yet valid, poll until it is
|
|
68
|
+
if (order.status !== 'valid' && order.url) {
|
|
69
|
+
const finalOrder = await this.waitForValidStatus({ url: order.url });
|
|
70
|
+
order.status = finalOrder.status;
|
|
71
|
+
order.certificate = finalOrder.certificate;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Download the certificate chain (PEM) from the order's certificate URL
|
|
77
|
+
*/
|
|
78
|
+
async getCertificate(order: IAcmeOrder): Promise<string> {
|
|
79
|
+
if (!order.certificate) {
|
|
80
|
+
throw new Error('Order does not have a certificate URL - finalize first');
|
|
81
|
+
}
|
|
82
|
+
const response = await this.httpClient.signedRequest(order.certificate, null);
|
|
83
|
+
// Certificate chain is returned as PEM text
|
|
84
|
+
return typeof response.data === 'string' ? response.data : response.data.toString();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Poll an ACME resource (order or challenge) until it reaches 'valid' or 'ready' status.
|
|
89
|
+
* Uses exponential backoff with Retry-After header support.
|
|
90
|
+
*/
|
|
91
|
+
async waitForValidStatus(
|
|
92
|
+
item: { url: string },
|
|
93
|
+
opts?: { maxAttempts?: number; initialDelayMs?: number },
|
|
94
|
+
): Promise<any> {
|
|
95
|
+
const maxAttempts = opts?.maxAttempts ?? 30;
|
|
96
|
+
const initialDelay = opts?.initialDelayMs ?? 1000;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
99
|
+
const response = await this.httpClient.signedRequest(item.url, null);
|
|
100
|
+
const body = response.data;
|
|
101
|
+
|
|
102
|
+
if (body.status === 'valid' || body.status === 'ready') {
|
|
103
|
+
return body;
|
|
104
|
+
}
|
|
105
|
+
if (body.status === 'invalid') {
|
|
106
|
+
const challengeError = body.challenges?.find((c: any) => c.error)?.error;
|
|
107
|
+
throw new AcmeError({
|
|
108
|
+
status: 0,
|
|
109
|
+
type: challengeError?.type || 'urn:ietf:params:acme:error:rejectedIdentifier',
|
|
110
|
+
detail: challengeError?.detail || JSON.stringify(body),
|
|
111
|
+
subproblems: challengeError?.subproblems,
|
|
112
|
+
url: item.url,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Respect Retry-After header, otherwise exponential backoff
|
|
117
|
+
const retryAfter = parseInt(response.headers['retry-after'] || '0', 10);
|
|
118
|
+
const delay =
|
|
119
|
+
retryAfter > 0 ? retryAfter * 1000 : Math.min(initialDelay * Math.pow(2, i), 30000);
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new Error(`Timeout waiting for valid status after ${maxAttempts} attempts`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACME Protocol interfaces per RFC 8555
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface IAcmeDirectory {
|
|
6
|
+
newNonce: string;
|
|
7
|
+
newAccount: string;
|
|
8
|
+
newOrder: string;
|
|
9
|
+
newAuthz?: string;
|
|
10
|
+
revokeCert?: string;
|
|
11
|
+
keyChange?: string;
|
|
12
|
+
meta?: IAcmeDirectoryMeta;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IAcmeDirectoryMeta {
|
|
16
|
+
termsOfService?: string;
|
|
17
|
+
website?: string;
|
|
18
|
+
caaIdentities?: string[];
|
|
19
|
+
externalAccountRequired?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IAcmeIdentifier {
|
|
23
|
+
type: 'dns';
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IAcmeAccount {
|
|
28
|
+
status: string;
|
|
29
|
+
contact?: string[];
|
|
30
|
+
termsOfServiceAgreed?: boolean;
|
|
31
|
+
orders?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IAcmeAccountCreateRequest {
|
|
35
|
+
termsOfServiceAgreed: boolean;
|
|
36
|
+
contact?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IAcmeOrder {
|
|
40
|
+
url: string;
|
|
41
|
+
status: string;
|
|
42
|
+
expires?: string;
|
|
43
|
+
identifiers: IAcmeIdentifier[];
|
|
44
|
+
authorizations: string[];
|
|
45
|
+
finalize: string;
|
|
46
|
+
certificate?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface IAcmeAuthorization {
|
|
50
|
+
identifier: IAcmeIdentifier;
|
|
51
|
+
status: string;
|
|
52
|
+
expires?: string;
|
|
53
|
+
challenges: IAcmeChallenge[];
|
|
54
|
+
wildcard?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface IAcmeChallenge {
|
|
58
|
+
type: string;
|
|
59
|
+
url: string;
|
|
60
|
+
status: string;
|
|
61
|
+
token: string;
|
|
62
|
+
validated?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IAcmeCsrOptions {
|
|
66
|
+
commonName: string;
|
|
67
|
+
altNames?: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IAcmeHttpResponse {
|
|
71
|
+
status: number;
|
|
72
|
+
headers: Record<string, string>;
|
|
73
|
+
data: any;
|
|
74
|
+
}
|
package/ts/acme/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { AcmeClient, type IAcmeClientOptions } from './acme.classes.client.js';
|
|
2
|
+
export { AcmeCrypto } from './acme.classes.crypto.js';
|
|
3
|
+
export { AcmeError } from './acme.classes.error.js';
|
|
4
|
+
export { ACME_DIRECTORY_URLS } from './acme.classes.directory.js';
|
|
5
|
+
export type {
|
|
6
|
+
IAcmeDirectory,
|
|
7
|
+
IAcmeDirectoryMeta,
|
|
8
|
+
IAcmeIdentifier,
|
|
9
|
+
IAcmeAccount,
|
|
10
|
+
IAcmeAccountCreateRequest,
|
|
11
|
+
IAcmeOrder,
|
|
12
|
+
IAcmeAuthorization,
|
|
13
|
+
IAcmeChallenge,
|
|
14
|
+
IAcmeCsrOptions,
|
|
15
|
+
IAcmeHttpResponse,
|
|
16
|
+
} from './acme.interfaces.js';
|
package/ts/certmanagers/mongo.ts
CHANGED
|
@@ -14,7 +14,6 @@ export class MongoCertManager implements ICertManager {
|
|
|
14
14
|
*/
|
|
15
15
|
constructor(mongoDescriptor: plugins.smartdata.IMongoDescriptor) {
|
|
16
16
|
this.db = new plugins.smartdata.SmartdataDb(mongoDescriptor);
|
|
17
|
-
// Use a single EasyStore document to hold all certs keyed by domainName
|
|
18
17
|
this.store = new plugins.smartdata.EasyStore<Record<string, any>>(
|
|
19
18
|
'smartacme-certs',
|
|
20
19
|
this.db,
|
package/ts/plugins.ts
CHANGED
|
@@ -9,21 +9,13 @@ import * as cloudflare from '@apiclient.xyz/cloudflare';
|
|
|
9
9
|
|
|
10
10
|
export { cloudflare };
|
|
11
11
|
|
|
12
|
-
// @
|
|
13
|
-
import * as typedserver from '@api.global/typedserver';
|
|
14
|
-
|
|
15
|
-
export { typedserver };
|
|
16
|
-
|
|
17
|
-
// @pushrocks scope
|
|
12
|
+
// @push.rocks scope
|
|
18
13
|
import * as lik from '@push.rocks/lik';
|
|
19
14
|
import * as smartdata from '@push.rocks/smartdata';
|
|
20
15
|
import * as smartdelay from '@push.rocks/smartdelay';
|
|
21
16
|
import * as smartdnsClient from '@push.rocks/smartdns/client';
|
|
22
|
-
import * as smartfile from '@push.rocks/smartfile';
|
|
23
17
|
import * as smartlog from '@push.rocks/smartlog';
|
|
24
18
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
|
25
|
-
import * as smartpromise from '@push.rocks/smartpromise';
|
|
26
|
-
import * as smartrequest from '@push.rocks/smartrequest';
|
|
27
19
|
import * as smartunique from '@push.rocks/smartunique';
|
|
28
20
|
import * as smartstring from '@push.rocks/smartstring';
|
|
29
21
|
import * as smarttime from '@push.rocks/smarttime';
|
|
@@ -33,11 +25,8 @@ export {
|
|
|
33
25
|
smartdata,
|
|
34
26
|
smartdelay,
|
|
35
27
|
smartdnsClient,
|
|
36
|
-
smartfile,
|
|
37
28
|
smartlog,
|
|
38
29
|
smartnetwork,
|
|
39
|
-
smartpromise,
|
|
40
|
-
smartrequest,
|
|
41
30
|
smartunique,
|
|
42
31
|
smartstring,
|
|
43
32
|
smarttime,
|
|
@@ -48,8 +37,8 @@ import * as tsclass from '@tsclass/tsclass';
|
|
|
48
37
|
|
|
49
38
|
export { tsclass };
|
|
50
39
|
|
|
51
|
-
//
|
|
52
|
-
import * as acme from 'acme
|
|
40
|
+
// acme protocol (custom implementation)
|
|
41
|
+
import * as acme from './acme/index.js';
|
|
53
42
|
|
|
54
43
|
export { acme };
|
|
55
44
|
// local handlers for challenge types
|
|
@@ -54,7 +54,7 @@ export class SmartAcme {
|
|
|
54
54
|
private options: ISmartAcmeOptions;
|
|
55
55
|
|
|
56
56
|
// the acme client
|
|
57
|
-
private client: plugins.acme.
|
|
57
|
+
private client: plugins.acme.AcmeClient;
|
|
58
58
|
private smartdns = new plugins.smartdnsClient.Smartdns({});
|
|
59
59
|
public logger: plugins.smartlog.Smartlog;
|
|
60
60
|
|
|
@@ -77,6 +77,9 @@ export class SmartAcme {
|
|
|
77
77
|
private challengePriority: string[];
|
|
78
78
|
// Map for coordinating concurrent certificate requests
|
|
79
79
|
private interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
|
80
|
+
// bound signal handlers so they can be removed on stop()
|
|
81
|
+
private boundSigintHandler: (() => void) | null = null;
|
|
82
|
+
private boundSigtermHandler: (() => void) | null = null;
|
|
80
83
|
|
|
81
84
|
constructor(optionsArg: ISmartAcmeOptions) {
|
|
82
85
|
this.options = optionsArg;
|
|
@@ -114,7 +117,7 @@ export class SmartAcme {
|
|
|
114
117
|
*/
|
|
115
118
|
public async start() {
|
|
116
119
|
this.privateKey =
|
|
117
|
-
this.options.accountPrivateKey ||
|
|
120
|
+
this.options.accountPrivateKey || plugins.acme.AcmeCrypto.createRsaPrivateKey();
|
|
118
121
|
|
|
119
122
|
// Initialize certificate manager
|
|
120
123
|
if (!this.options.certManager) {
|
|
@@ -127,15 +130,18 @@ export class SmartAcme {
|
|
|
127
130
|
this.certmatcher = new SmartacmeCertMatcher();
|
|
128
131
|
|
|
129
132
|
// ACME Client
|
|
130
|
-
this.client = new plugins.acme.
|
|
133
|
+
this.client = new plugins.acme.AcmeClient({
|
|
131
134
|
directoryUrl: (() => {
|
|
132
135
|
if (this.options.environment === 'production') {
|
|
133
|
-
return plugins.acme.
|
|
136
|
+
return plugins.acme.ACME_DIRECTORY_URLS.letsencrypt.production;
|
|
134
137
|
} else {
|
|
135
|
-
return plugins.acme.
|
|
138
|
+
return plugins.acme.ACME_DIRECTORY_URLS.letsencrypt.staging;
|
|
136
139
|
}
|
|
137
140
|
})(),
|
|
138
|
-
|
|
141
|
+
accountKeyPem: this.privateKey,
|
|
142
|
+
logger: (level, message, data) => {
|
|
143
|
+
this.logger.log(level as any, message, data);
|
|
144
|
+
},
|
|
139
145
|
});
|
|
140
146
|
|
|
141
147
|
/* Register account */
|
|
@@ -143,20 +149,31 @@ export class SmartAcme {
|
|
|
143
149
|
termsOfServiceAgreed: true,
|
|
144
150
|
contact: [`mailto:${this.options.accountEmail}`],
|
|
145
151
|
});
|
|
146
|
-
// Setup graceful shutdown handlers
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
// Setup graceful shutdown handlers (store references for removal in stop())
|
|
153
|
+
this.boundSigintHandler = () => this.handleSignal('SIGINT');
|
|
154
|
+
this.boundSigtermHandler = () => this.handleSignal('SIGTERM');
|
|
155
|
+
process.on('SIGINT', this.boundSigintHandler);
|
|
156
|
+
process.on('SIGTERM', this.boundSigtermHandler);
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
/**
|
|
152
160
|
* Stops the SmartAcme instance and closes certificate store connections.
|
|
153
161
|
*/
|
|
154
162
|
public async stop() {
|
|
163
|
+
// Remove signal handlers so the process can exit cleanly
|
|
164
|
+
if (this.boundSigintHandler) {
|
|
165
|
+
process.removeListener('SIGINT', this.boundSigintHandler);
|
|
166
|
+
this.boundSigintHandler = null;
|
|
167
|
+
}
|
|
168
|
+
if (this.boundSigtermHandler) {
|
|
169
|
+
process.removeListener('SIGTERM', this.boundSigtermHandler);
|
|
170
|
+
this.boundSigtermHandler = null;
|
|
171
|
+
}
|
|
155
172
|
if (this.certmanager && typeof (this.certmanager as any).close === 'function') {
|
|
156
173
|
await (this.certmanager as any).close();
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
|
-
/** Retry helper with exponential backoff */
|
|
176
|
+
/** Retry helper with exponential backoff and AcmeError awareness */
|
|
160
177
|
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
|
|
161
178
|
let attempt = 0;
|
|
162
179
|
let delay = this.retryOptions.minTimeoutMs;
|
|
@@ -164,6 +181,19 @@ export class SmartAcme {
|
|
|
164
181
|
try {
|
|
165
182
|
return await operation();
|
|
166
183
|
} catch (err) {
|
|
184
|
+
// Check if it's a non-retryable ACME error — throw immediately
|
|
185
|
+
if (err instanceof plugins.acme.AcmeError) {
|
|
186
|
+
if (!err.isRetryable) {
|
|
187
|
+
await this.logger.log('error', `Operation ${operationName} failed with non-retryable error (${err.type}, HTTP ${err.status}) at ${err.url}`, err);
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
// For rate-limited errors, use server-specified Retry-After delay
|
|
191
|
+
if (err.isRateLimited && err.retryAfter > 0) {
|
|
192
|
+
delay = err.retryAfter * 1000;
|
|
193
|
+
await this.logger.log('warn', `Operation ${operationName} rate-limited, Retry-After: ${err.retryAfter}s`, err);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
167
197
|
attempt++;
|
|
168
198
|
if (attempt > this.retryOptions.retries) {
|
|
169
199
|
await this.logger.log('error', `Operation ${operationName} failed after ${attempt} attempts`, err);
|
|
@@ -221,8 +251,12 @@ export class SmartAcme {
|
|
|
221
251
|
* * retrieve it from the databse and return it
|
|
222
252
|
*
|
|
223
253
|
* @param domainArg
|
|
254
|
+
* @param options Optional configuration for certificate generation
|
|
224
255
|
*/
|
|
225
|
-
public async getCertificateForDomain(
|
|
256
|
+
public async getCertificateForDomain(
|
|
257
|
+
domainArg: string,
|
|
258
|
+
options?: { includeWildcard?: boolean }
|
|
259
|
+
): Promise<SmartacmeCert> {
|
|
226
260
|
// Determine if this is a wildcard request (e.g., '*.example.com').
|
|
227
261
|
const isWildcardRequest = domainArg.startsWith('*.');
|
|
228
262
|
// Determine the base domain for certificate retrieval/issuance.
|
|
@@ -259,12 +293,32 @@ export class SmartAcme {
|
|
|
259
293
|
// lets make sure others get the same interest
|
|
260
294
|
const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
|
|
261
295
|
|
|
296
|
+
// Build identifiers array based on request
|
|
297
|
+
const identifiers = [];
|
|
298
|
+
|
|
299
|
+
if (isWildcardRequest) {
|
|
300
|
+
// If requesting a wildcard directly, only add the wildcard
|
|
301
|
+
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
|
|
302
|
+
} else {
|
|
303
|
+
// Add the regular domain
|
|
304
|
+
identifiers.push({ type: 'dns', value: certDomainName });
|
|
305
|
+
|
|
306
|
+
// Only add wildcard if explicitly requested
|
|
307
|
+
if (options?.includeWildcard) {
|
|
308
|
+
const hasDnsHandler = this.challengeHandlers.some((h) =>
|
|
309
|
+
h.getSupportedTypes().includes('dns-01'),
|
|
310
|
+
);
|
|
311
|
+
if (!hasDnsHandler) {
|
|
312
|
+
this.logger.log('warn', 'Wildcard certificate requested but no DNS-01 handler available. Skipping wildcard.');
|
|
313
|
+
} else {
|
|
314
|
+
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
262
319
|
/* Place new order with retry */
|
|
263
320
|
const order = await this.retry(() => this.client.createOrder({
|
|
264
|
-
identifiers
|
|
265
|
-
{ type: 'dns', value: certDomainName },
|
|
266
|
-
{ type: 'dns', value: `*.${certDomainName}` },
|
|
267
|
-
],
|
|
321
|
+
identifiers,
|
|
268
322
|
}), 'createOrder');
|
|
269
323
|
|
|
270
324
|
/* Get authorizations and select challenges */
|
|
@@ -323,11 +377,6 @@ export class SmartAcme {
|
|
|
323
377
|
this.logger.log('info', 'Cooling down for 1 minute before ACME verification');
|
|
324
378
|
await plugins.smartdelay.delayFor(60000);
|
|
325
379
|
}
|
|
326
|
-
// Official ACME verification (ensures challenge is publicly reachable)
|
|
327
|
-
await this.retry(
|
|
328
|
-
() => this.client.verifyChallenge(authz, selectedChallengeArg),
|
|
329
|
-
`${type}.verifyChallenge`,
|
|
330
|
-
);
|
|
331
380
|
// Notify ACME server to complete the challenge
|
|
332
381
|
await this.retry(
|
|
333
382
|
() => this.client.completeChallenge(selectedChallengeArg),
|
|
@@ -359,9 +408,25 @@ export class SmartAcme {
|
|
|
359
408
|
}
|
|
360
409
|
|
|
361
410
|
/* Finalize order */
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
411
|
+
const csrDomains = [];
|
|
412
|
+
let commonName: string;
|
|
413
|
+
|
|
414
|
+
if (isWildcardRequest) {
|
|
415
|
+
// For wildcard requests, use wildcard as common name
|
|
416
|
+
commonName = `*.${certDomainName}`;
|
|
417
|
+
csrDomains.push(certDomainName); // Add base domain as alt name
|
|
418
|
+
} else {
|
|
419
|
+
// For regular requests, use base domain as common name
|
|
420
|
+
commonName = certDomainName;
|
|
421
|
+
if (options?.includeWildcard && identifiers.some(id => id.value === `*.${certDomainName}`)) {
|
|
422
|
+
// If wildcard was successfully added, include it as alt name
|
|
423
|
+
csrDomains.push(`*.${certDomainName}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const [key, csr] = await plugins.acme.AcmeCrypto.createCsr({
|
|
428
|
+
commonName,
|
|
429
|
+
altNames: csrDomains,
|
|
365
430
|
});
|
|
366
431
|
|
|
367
432
|
await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import * as plugins from './smartacme.plugins.js';
|
|
2
|
-
import type { ICertManager } from './interfaces/certmanager.js';
|
|
3
|
-
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
|
4
|
-
/**
|
|
5
|
-
* In-memory certificate manager for mongoless mode.
|
|
6
|
-
* Stores certificates in memory only and does not connect to MongoDB.
|
|
7
|
-
*/
|
|
8
|
-
export declare class MemoryCertManager implements ICertManager {
|
|
9
|
-
interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
|
10
|
-
private certs;
|
|
11
|
-
constructor();
|
|
12
|
-
init(): Promise<void>;
|
|
13
|
-
retrieveCertificate(domainName: string): Promise<SmartacmeCert | null>;
|
|
14
|
-
storeCertificate(cert: SmartacmeCert): Promise<void>;
|
|
15
|
-
deleteCertificate(domainName: string): Promise<void>;
|
|
16
|
-
close(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Wipe all certificates from the in-memory store (for testing)
|
|
19
|
-
*/
|
|
20
|
-
wipe(): Promise<void>;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* MongoDB-backed certificate manager using EasyStore from smartdata.
|
|
24
|
-
*/
|
|
25
|
-
export declare class MongoCertManager implements ICertManager {
|
|
26
|
-
interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
|
27
|
-
private db;
|
|
28
|
-
private store;
|
|
29
|
-
/**
|
|
30
|
-
* @param mongoDescriptor MongoDB connection settings
|
|
31
|
-
*/
|
|
32
|
-
constructor(mongoDescriptor: plugins.smartdata.IMongoDescriptor);
|
|
33
|
-
init(): Promise<void>;
|
|
34
|
-
retrieveCertificate(domainName: string): Promise<SmartacmeCert | null>;
|
|
35
|
-
storeCertificate(cert: SmartacmeCert): Promise<void>;
|
|
36
|
-
deleteCertificate(domainName: string): Promise<void>;
|
|
37
|
-
close(): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Wipe all certificates from the persistent store (for integration testing)
|
|
40
|
-
*/
|
|
41
|
-
wipe(): Promise<void>;
|
|
42
|
-
}
|
package/dist_ts/certmanagers.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import * as plugins from './smartacme.plugins.js';
|
|
2
|
-
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
|
3
|
-
/**
|
|
4
|
-
* In-memory certificate manager for mongoless mode.
|
|
5
|
-
* Stores certificates in memory only and does not connect to MongoDB.
|
|
6
|
-
*/
|
|
7
|
-
export class MemoryCertManager {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.certs = new Map();
|
|
10
|
-
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
|
11
|
-
}
|
|
12
|
-
async init() {
|
|
13
|
-
// no-op for in-memory store
|
|
14
|
-
}
|
|
15
|
-
async retrieveCertificate(domainName) {
|
|
16
|
-
return this.certs.get(domainName) ?? null;
|
|
17
|
-
}
|
|
18
|
-
async storeCertificate(cert) {
|
|
19
|
-
this.certs.set(cert.domainName, cert);
|
|
20
|
-
const interest = this.interestMap.findInterest(cert.domainName);
|
|
21
|
-
if (interest) {
|
|
22
|
-
interest.fullfillInterest(cert);
|
|
23
|
-
interest.markLost();
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
async deleteCertificate(domainName) {
|
|
27
|
-
this.certs.delete(domainName);
|
|
28
|
-
}
|
|
29
|
-
async close() {
|
|
30
|
-
// no-op
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Wipe all certificates from the in-memory store (for testing)
|
|
34
|
-
*/
|
|
35
|
-
async wipe() {
|
|
36
|
-
this.certs.clear();
|
|
37
|
-
// reset interest map
|
|
38
|
-
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* MongoDB-backed certificate manager using EasyStore from smartdata.
|
|
43
|
-
*/
|
|
44
|
-
export class MongoCertManager {
|
|
45
|
-
/**
|
|
46
|
-
* @param mongoDescriptor MongoDB connection settings
|
|
47
|
-
*/
|
|
48
|
-
constructor(mongoDescriptor) {
|
|
49
|
-
this.db = new plugins.smartdata.SmartdataDb(mongoDescriptor);
|
|
50
|
-
// Use a single EasyStore document to hold all certs keyed by domainName
|
|
51
|
-
this.store = new plugins.smartdata.EasyStore('smartacme-certs', this.db);
|
|
52
|
-
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
|
53
|
-
}
|
|
54
|
-
async init() {
|
|
55
|
-
await this.db.init();
|
|
56
|
-
}
|
|
57
|
-
async retrieveCertificate(domainName) {
|
|
58
|
-
const data = await this.store.readKey(domainName);
|
|
59
|
-
return data ? new SmartacmeCert(data) : null;
|
|
60
|
-
}
|
|
61
|
-
async storeCertificate(cert) {
|
|
62
|
-
// write plain object for persistence
|
|
63
|
-
await this.store.writeKey(cert.domainName, { ...cert });
|
|
64
|
-
const interest = this.interestMap.findInterest(cert.domainName);
|
|
65
|
-
if (interest) {
|
|
66
|
-
interest.fullfillInterest(cert);
|
|
67
|
-
interest.markLost();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async deleteCertificate(domainName) {
|
|
71
|
-
await this.store.deleteKey(domainName);
|
|
72
|
-
}
|
|
73
|
-
async close() {
|
|
74
|
-
await this.db.close();
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Wipe all certificates from the persistent store (for integration testing)
|
|
78
|
-
*/
|
|
79
|
-
async wipe() {
|
|
80
|
-
// clear all keys in the easy store
|
|
81
|
-
await this.store.wipe();
|
|
82
|
-
// reset interest map
|
|
83
|
-
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2VydG1hbmFnZXJzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvY2VydG1hbmFnZXJzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sd0JBQXdCLENBQUM7QUFFbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBRTVEOzs7R0FHRztBQUNILE1BQU0sT0FBTyxpQkFBaUI7SUFJNUI7UUFGUSxVQUFLLEdBQStCLElBQUksR0FBRyxFQUFFLENBQUM7UUFHcEQsSUFBSSxDQUFDLFdBQVcsR0FBRyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsV0FBVyxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUNyRSxDQUFDO0lBRU0sS0FBSyxDQUFDLElBQUk7UUFDZiw0QkFBNEI7SUFDOUIsQ0FBQztJQUVNLEtBQUssQ0FBQyxtQkFBbUIsQ0FBQyxVQUFrQjtRQUNqRCxPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLElBQUksQ0FBQztJQUM1QyxDQUFDO0lBRU0sS0FBSyxDQUFDLGdCQUFnQixDQUFDLElBQW1CO1FBQy9DLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsSUFBSSxDQUFDLENBQUM7UUFDdEMsTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLFdBQVcsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ2hFLElBQUksUUFBUSxFQUFFLENBQUM7WUFDYixRQUFRLENBQUMsZ0JBQWdCLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDaEMsUUFBUSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ3RCLENBQUM7SUFDSCxDQUFDO0lBRU0sS0FBSyxDQUFDLGlCQUFpQixDQUFDLFVBQWtCO1FBQy9DLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxD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
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import * as plugins from './smartacme.plugins.js';
|
|
2
|
-
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
|
3
|
-
import { SmartAcme } from './smartacme.classes.smartacme.js';
|
|
4
|
-
export declare class SmartacmeCertManager {
|
|
5
|
-
static activeDB: plugins.smartdata.SmartdataDb;
|
|
6
|
-
private mongoDescriptor;
|
|
7
|
-
smartdataDb: plugins.smartdata.SmartdataDb;
|
|
8
|
-
interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
|
9
|
-
constructor(smartAcmeArg: SmartAcme, optionsArg: {
|
|
10
|
-
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
|
|
11
|
-
});
|
|
12
|
-
init(): Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* retrieves a certificate
|
|
15
|
-
* @returns the Cert class or null
|
|
16
|
-
* @param certDomainNameArg the domain Name to retrieve the vcertificate for
|
|
17
|
-
*/
|
|
18
|
-
retrieveCertificate(certDomainNameArg: string): Promise<SmartacmeCert>;
|
|
19
|
-
/**
|
|
20
|
-
* stores the certificate
|
|
21
|
-
* @param optionsArg
|
|
22
|
-
*/
|
|
23
|
-
storeCertificate(optionsArg: plugins.tsclass.network.ICert): Promise<void>;
|
|
24
|
-
deleteCertificate(certDomainNameArg: string): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
* Close underlying MongoDB connection
|
|
27
|
-
*/
|
|
28
|
-
close(): Promise<void>;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* In-memory certificate manager for mongoless mode.
|
|
32
|
-
* Stores certificates in memory only and does not connect to MongoDB.
|
|
33
|
-
*/
|
|
34
|
-
export declare class MemoryCertManager {
|
|
35
|
-
interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
|
36
|
-
private certs;
|
|
37
|
-
constructor();
|
|
38
|
-
init(): Promise<void>;
|
|
39
|
-
retrieveCertificate(certDomainName: string): Promise<SmartacmeCert>;
|
|
40
|
-
storeCertificate(optionsArg: plugins.tsclass.network.ICert): Promise<void>;
|
|
41
|
-
deleteCertificate(certDomainName: string): Promise<void>;
|
|
42
|
-
close(): Promise<void>;
|
|
43
|
-
}
|