@certd/acme-client 1.22.6 → 1.24.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/package.json +7 -7
- package/src/api.js +17 -1
- package/src/auto.js +50 -7
- package/src/axios.js +87 -1
- package/src/client.js +12 -1
- package/src/crypto/forge.js +12 -5
- package/src/http.js +6 -3
- package/src/util.js +45 -8
- package/types/index.d.ts +2 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Simple and unopinionated ACME client",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "nmorsman",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.24.0",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"types": "types/index.d.ts",
|
|
9
9
|
"license": "MIT",
|
|
@@ -16,24 +16,24 @@
|
|
|
16
16
|
"types"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@peculiar/x509": "^1.
|
|
19
|
+
"@peculiar/x509": "^1.11.0",
|
|
20
20
|
"asn1js": "^3.0.5",
|
|
21
21
|
"axios": "^1.7.2",
|
|
22
|
-
"debug": "^4.
|
|
22
|
+
"debug": "^4.3.5",
|
|
23
23
|
"https-proxy-agent": "^7.0.4",
|
|
24
24
|
"node-forge": "^1.3.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@types/node": "^20.
|
|
27
|
+
"@types/node": "^20.14.10",
|
|
28
28
|
"chai": "^4.4.1",
|
|
29
29
|
"chai-as-promised": "^7.1.2",
|
|
30
30
|
"eslint": "^8.57.0",
|
|
31
31
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
32
32
|
"eslint-plugin-import": "^2.29.1",
|
|
33
33
|
"jsdoc-to-markdown": "^8.0.1",
|
|
34
|
-
"mocha": "^10.
|
|
34
|
+
"mocha": "^10.6.0",
|
|
35
35
|
"nock": "^13.5.4",
|
|
36
|
-
"tsd": "^0.31.
|
|
36
|
+
"tsd": "^0.31.1",
|
|
37
37
|
"typescript": "^4.8.4",
|
|
38
38
|
"uuid": "^8.3.2"
|
|
39
39
|
},
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"bugs": {
|
|
60
60
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
|
61
61
|
},
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "f17b08ddab8245108dda6e86d59d43ebbd68b9ba"
|
|
63
63
|
}
|
package/src/api.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const util = require('./util');
|
|
6
|
+
const { log } = require('./logger');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* AcmeApi
|
|
@@ -17,6 +18,21 @@ class AcmeApi {
|
|
|
17
18
|
this.accountUrl = accountUrl;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
getLocationFromHeader(resp) {
|
|
22
|
+
let locationUrl = resp.headers.location;
|
|
23
|
+
const mapping = this.http.urlMapping;
|
|
24
|
+
if (mapping.mappings) {
|
|
25
|
+
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
|
26
|
+
for (const key in mapping.mappings) {
|
|
27
|
+
const url = mapping.mappings[key];
|
|
28
|
+
if (locationUrl.indexOf(url) > -1) {
|
|
29
|
+
locationUrl = locationUrl.replace(url, key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return locationUrl;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
/**
|
|
21
37
|
* Get account URL
|
|
22
38
|
*
|
|
@@ -103,7 +119,7 @@ class AcmeApi {
|
|
|
103
119
|
|
|
104
120
|
/* Set account URL */
|
|
105
121
|
if (resp.headers.location) {
|
|
106
|
-
this.accountUrl = resp
|
|
122
|
+
this.accountUrl = this.getLocationFromHeader(resp);
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
return resp;
|
package/src/auto.js
CHANGED
|
@@ -137,9 +137,13 @@ module.exports = async (client, userOpts) => {
|
|
|
137
137
|
}
|
|
138
138
|
else {
|
|
139
139
|
log(`[auto] [${d}] Running challenge verification`);
|
|
140
|
-
|
|
140
|
+
try {
|
|
141
|
+
await client.verifyChallenge(authz, challenge);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
log(`[auto] [${d}] challenge verification threw error: ${e.message}`);
|
|
145
|
+
}
|
|
141
146
|
}
|
|
142
|
-
|
|
143
147
|
/* Complete challenge and wait for valid status */
|
|
144
148
|
log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`);
|
|
145
149
|
await client.completeChallenge(challenge);
|
|
@@ -170,11 +174,41 @@ module.exports = async (client, userOpts) => {
|
|
|
170
174
|
throw e;
|
|
171
175
|
}
|
|
172
176
|
};
|
|
177
|
+
const domainSets = [];
|
|
173
178
|
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
authorizations.forEach((authz) => {
|
|
180
|
+
const d = authz.identifier.value;
|
|
181
|
+
let setd = false;
|
|
182
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
183
|
+
for (const group of domainSets) {
|
|
184
|
+
if (!group[d]) {
|
|
185
|
+
group[d] = authz;
|
|
186
|
+
setd = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!setd) {
|
|
190
|
+
const group = {};
|
|
191
|
+
group[d] = authz;
|
|
192
|
+
domainSets.push(group);
|
|
193
|
+
}
|
|
176
194
|
});
|
|
177
195
|
|
|
196
|
+
const allChallengePromises = [];
|
|
197
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
198
|
+
for (const domainSet of domainSets) {
|
|
199
|
+
const challengePromises = [];
|
|
200
|
+
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
|
201
|
+
for (const domain in domainSet) {
|
|
202
|
+
const authz = domainSet[domain];
|
|
203
|
+
challengePromises.push(async () => {
|
|
204
|
+
log(`[auto] [${domain}] Starting challenge`);
|
|
205
|
+
await challengeFunc(authz);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
allChallengePromises.push(challengePromises);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
log(`[auto] challengeGroups:${allChallengePromises.length}`);
|
|
178
212
|
function runAllPromise(tasks) {
|
|
179
213
|
let promise = Promise.resolve();
|
|
180
214
|
tasks.forEach((task) => {
|
|
@@ -195,9 +229,18 @@ module.exports = async (client, userOpts) => {
|
|
|
195
229
|
}
|
|
196
230
|
|
|
197
231
|
try {
|
|
198
|
-
log(
|
|
199
|
-
|
|
200
|
-
|
|
232
|
+
log(`开始challenge,共${allChallengePromises.length}组`);
|
|
233
|
+
let i = 0;
|
|
234
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
235
|
+
for (const challengePromises of allChallengePromises) {
|
|
236
|
+
i += 1;
|
|
237
|
+
log(`开始第${i}组`);
|
|
238
|
+
if (opts.signal && opts.signal.aborted) {
|
|
239
|
+
throw new Error('用户取消');
|
|
240
|
+
}
|
|
241
|
+
// eslint-disable-next-line no-await-in-loop
|
|
242
|
+
await runPromisePa(challengePromises);
|
|
243
|
+
}
|
|
201
244
|
log('challenge结束');
|
|
202
245
|
|
|
203
246
|
// log('[auto] Waiting for challenge valid status');
|
package/src/axios.js
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const axios = require('axios');
|
|
6
|
+
const { parseRetryAfterHeader } = require('./util');
|
|
7
|
+
const { log } = require('./logger');
|
|
6
8
|
const pkg = require('./../package.json');
|
|
7
9
|
|
|
10
|
+
const { AxiosError } = axios;
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
|
-
*
|
|
13
|
+
* Defaults
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
const instance = axios.create();
|
|
@@ -19,6 +23,9 @@ instance.defaults.acmeSettings = {
|
|
|
19
23
|
httpChallengePort: 80,
|
|
20
24
|
httpsChallengePort: 443,
|
|
21
25
|
tlsAlpnChallengePort: 443,
|
|
26
|
+
|
|
27
|
+
retryMaxAttempts: 5,
|
|
28
|
+
retryDefaultDelay: 5,
|
|
22
29
|
};
|
|
23
30
|
// instance.defaults.proxy = {
|
|
24
31
|
// host: '192.168.34.139',
|
|
@@ -33,6 +40,85 @@ instance.defaults.acmeSettings = {
|
|
|
33
40
|
|
|
34
41
|
instance.defaults.adapter = 'http';
|
|
35
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Retry requests on server errors or when rate limited
|
|
45
|
+
*
|
|
46
|
+
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.6
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
function isRetryableError(error) {
|
|
50
|
+
return (error.code !== 'ECONNABORTED')
|
|
51
|
+
&& (error.code !== 'ERR_NOCK_NO_MATCH')
|
|
52
|
+
&& (!error.response
|
|
53
|
+
|| (error.response.status === 429)
|
|
54
|
+
|| ((error.response.status >= 500) && (error.response.status <= 599)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* https://github.com/axios/axios/blob/main/lib/core/settle.js */
|
|
58
|
+
function validateStatus(response) {
|
|
59
|
+
const validator = response.config.retryValidateStatus;
|
|
60
|
+
|
|
61
|
+
if (!response.status || !validator || validator(response.status)) {
|
|
62
|
+
return response;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new AxiosError(
|
|
66
|
+
`Request failed with status code ${response.status}`,
|
|
67
|
+
(Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
|
|
68
|
+
response.config,
|
|
69
|
+
response.request,
|
|
70
|
+
response,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Pass all responses through the error interceptor */
|
|
75
|
+
instance.interceptors.request.use((config) => {
|
|
76
|
+
if (!('retryValidateStatus' in config)) {
|
|
77
|
+
config.retryValidateStatus = config.validateStatus;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
config.validateStatus = () => false;
|
|
81
|
+
return config;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/* Handle request retries if applicable */
|
|
85
|
+
instance.interceptors.response.use(null, async (error) => {
|
|
86
|
+
const { config, response } = error;
|
|
87
|
+
|
|
88
|
+
if (!config) {
|
|
89
|
+
return Promise.reject(error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Pick up errors we want to retry */
|
|
93
|
+
if (isRetryableError(error)) {
|
|
94
|
+
const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings;
|
|
95
|
+
config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1;
|
|
96
|
+
|
|
97
|
+
if (config.retryAttempt <= retryMaxAttempts) {
|
|
98
|
+
const code = response ? `HTTP ${response.status}` : error.code;
|
|
99
|
+
log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`);
|
|
100
|
+
|
|
101
|
+
/* Attempt to parse Retry-After header, fallback to default delay */
|
|
102
|
+
let retryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0;
|
|
103
|
+
|
|
104
|
+
if (retryAfter > 0) {
|
|
105
|
+
log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${retryAfter} seconds`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
retryAfter = (retryDefaultDelay * config.retryAttempt);
|
|
109
|
+
log(`Unable to locate or parse retry-after response header, waiting ${retryAfter} seconds`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Wait and retry the request */
|
|
113
|
+
await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); });
|
|
114
|
+
return instance(config);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Validate and return response */
|
|
119
|
+
return validateStatus(response);
|
|
120
|
+
});
|
|
121
|
+
|
|
36
122
|
/**
|
|
37
123
|
* Export instance
|
|
38
124
|
*/
|
package/src/client.js
CHANGED
|
@@ -300,7 +300,8 @@ class AcmeClient {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
/* Add URL to response */
|
|
303
|
-
resp.data.url =
|
|
303
|
+
resp.data.url = this.api.getLocationFromHeader(resp);
|
|
304
|
+
|
|
304
305
|
return resp.data;
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -490,6 +491,9 @@ class AcmeClient {
|
|
|
490
491
|
const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);
|
|
491
492
|
|
|
492
493
|
const verifyFn = async () => {
|
|
494
|
+
if (this.opts.signal && this.opts.signal.aborted) {
|
|
495
|
+
throw new Error('用户取消');
|
|
496
|
+
}
|
|
493
497
|
await verify[challenge.type](authz, challenge, keyAuthorization);
|
|
494
498
|
};
|
|
495
499
|
|
|
@@ -513,6 +517,9 @@ class AcmeClient {
|
|
|
513
517
|
*/
|
|
514
518
|
|
|
515
519
|
async completeChallenge(challenge) {
|
|
520
|
+
if (this.opts.signal && this.opts.signal.aborted) {
|
|
521
|
+
throw new Error('用户取消');
|
|
522
|
+
}
|
|
516
523
|
const resp = await this.api.completeChallenge(challenge.url, {});
|
|
517
524
|
return resp.data;
|
|
518
525
|
}
|
|
@@ -550,6 +557,10 @@ class AcmeClient {
|
|
|
550
557
|
}
|
|
551
558
|
|
|
552
559
|
const verifyFn = async (abort) => {
|
|
560
|
+
if (this.opts.signal && this.opts.signal.aborted) {
|
|
561
|
+
throw new Error('用户取消');
|
|
562
|
+
}
|
|
563
|
+
|
|
553
564
|
const resp = await this.api.apiRequest(item.url, null, [200]);
|
|
554
565
|
|
|
555
566
|
/* Verify status */
|
package/src/crypto/forge.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const net = require('net');
|
|
11
11
|
const { promisify } = require('util');
|
|
12
12
|
const forge = require('node-forge');
|
|
13
|
+
const { createPrivateEcdsaKey, getPublicKey } = require('./index');
|
|
13
14
|
|
|
14
15
|
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
|
|
15
16
|
|
|
@@ -378,13 +379,17 @@ function formatCsrAltNames(altNames) {
|
|
|
378
379
|
* }, certificateKey);
|
|
379
380
|
*/
|
|
380
381
|
|
|
381
|
-
exports.createCsr = async (data,
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
exports.createCsr = async (data, keyType = null) => {
|
|
383
|
+
let key = null;
|
|
384
|
+
if (keyType === 'ec') {
|
|
385
|
+
key = await createPrivateEcdsaKey();
|
|
384
386
|
}
|
|
385
|
-
else
|
|
386
|
-
key =
|
|
387
|
+
else {
|
|
388
|
+
key = await createPrivateKey(data.keySize);
|
|
387
389
|
}
|
|
390
|
+
// else if (!Buffer.isBuffer(key)) {
|
|
391
|
+
// key = Buffer.from(key);
|
|
392
|
+
// }
|
|
388
393
|
|
|
389
394
|
if (typeof data.altNames === 'undefined') {
|
|
390
395
|
data.altNames = [];
|
|
@@ -396,6 +401,8 @@ exports.createCsr = async (data, key = null) => {
|
|
|
396
401
|
const privateKey = forge.pki.privateKeyFromPem(key);
|
|
397
402
|
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
|
|
398
403
|
csr.publicKey = publicKey;
|
|
404
|
+
// const privateKey = key;
|
|
405
|
+
// csr.publicKey = getPublicKey(key);
|
|
399
406
|
|
|
400
407
|
/* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */
|
|
401
408
|
if (data.commonName && !data.altNames.includes(data.commonName)) {
|
package/src/http.js
CHANGED
|
@@ -55,7 +55,7 @@ class HttpClient {
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
async request(url, method, opts = {}) {
|
|
58
|
-
if (this.urlMapping && this.urlMapping.
|
|
58
|
+
if (this.urlMapping && this.urlMapping.mappings) {
|
|
59
59
|
// eslint-disable-next-line no-restricted-syntax
|
|
60
60
|
for (const key in this.urlMapping.mappings) {
|
|
61
61
|
if (url.includes(key)) {
|
|
@@ -93,9 +93,11 @@ class HttpClient {
|
|
|
93
93
|
*/
|
|
94
94
|
|
|
95
95
|
async getDirectory() {
|
|
96
|
-
const
|
|
96
|
+
const now = Math.floor(Date.now() / 1000);
|
|
97
|
+
const age = (now - this.directoryTimestamp);
|
|
97
98
|
|
|
98
99
|
if (!this.directoryCache || (age > this.directoryMaxAge)) {
|
|
100
|
+
log(`Refreshing ACME directory, age: ${age}`);
|
|
99
101
|
const resp = await this.request(this.directoryUrl, 'get');
|
|
100
102
|
|
|
101
103
|
if (resp.status >= 400) {
|
|
@@ -107,6 +109,7 @@ class HttpClient {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
this.directoryCache = resp.data;
|
|
112
|
+
this.directoryTimestamp = now;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
return this.directoryCache;
|
|
@@ -131,7 +134,7 @@ class HttpClient {
|
|
|
131
134
|
*
|
|
132
135
|
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
|
|
133
136
|
*
|
|
134
|
-
* @returns {Promise<string>}
|
|
137
|
+
* @returns {Promise<string>} Nonce
|
|
135
138
|
*/
|
|
136
139
|
|
|
137
140
|
async getNonce() {
|
package/src/util.js
CHANGED
|
@@ -84,9 +84,12 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Parse URLs from
|
|
87
|
+
* Parse URLs from Link header
|
|
88
88
|
*
|
|
89
|
-
*
|
|
89
|
+
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
|
|
90
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
|
|
91
|
+
*
|
|
92
|
+
* @param {string} header Header contents
|
|
90
93
|
* @param {string} rel Link relation, default: `alternate`
|
|
91
94
|
* @returns {string[]} Array of URLs
|
|
92
95
|
*/
|
|
@@ -102,6 +105,37 @@ function parseLinkHeader(header, rel = 'alternate') {
|
|
|
102
105
|
return results.filter((r) => r);
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Parse date or duration from Retry-After header
|
|
110
|
+
*
|
|
111
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
|
112
|
+
*
|
|
113
|
+
* @param {string} header Header contents
|
|
114
|
+
* @returns {number} Retry duration in seconds
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
function parseRetryAfterHeader(header) {
|
|
118
|
+
const sec = parseInt(header, 10);
|
|
119
|
+
const date = new Date(header);
|
|
120
|
+
|
|
121
|
+
/* Seconds into the future */
|
|
122
|
+
if (Number.isSafeInteger(sec) && (sec > 0)) {
|
|
123
|
+
return sec;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Future date string */
|
|
127
|
+
if (date instanceof Date && !Number.isNaN(date)) {
|
|
128
|
+
const now = new Date();
|
|
129
|
+
const diff = Math.ceil((date.getTime() - now.getTime()) / 1000);
|
|
130
|
+
|
|
131
|
+
if (diff > 0) {
|
|
132
|
+
return diff;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
/**
|
|
106
140
|
* Find certificate chain with preferred issuer common name
|
|
107
141
|
* - If issuer is found in multiple chains, the closest to root wins
|
|
@@ -161,14 +195,16 @@ function findCertificateChainForIssuer(chains, issuer) {
|
|
|
161
195
|
function formatResponseError(resp) {
|
|
162
196
|
let result;
|
|
163
197
|
|
|
164
|
-
if (resp.data
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
if (resp.data) {
|
|
199
|
+
if (resp.data.error) {
|
|
200
|
+
result = resp.data.error.detail || resp.data.error;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
result = resp.data.detail || JSON.stringify(resp.data);
|
|
204
|
+
}
|
|
169
205
|
}
|
|
170
206
|
|
|
171
|
-
return result.replace(/\n/g, '');
|
|
207
|
+
return (result || '').replace(/\n/g, '');
|
|
172
208
|
}
|
|
173
209
|
|
|
174
210
|
/**
|
|
@@ -296,6 +332,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
|
|
|
296
332
|
module.exports = {
|
|
297
333
|
retry,
|
|
298
334
|
parseLinkHeader,
|
|
335
|
+
parseRetryAfterHeader,
|
|
299
336
|
findCertificateChainForIssuer,
|
|
300
337
|
formatResponseError,
|
|
301
338
|
getAuthoritativeDnsResolver,
|
package/types/index.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export interface ClientOptions {
|
|
|
45
45
|
backoffMin?: number;
|
|
46
46
|
backoffMax?: number;
|
|
47
47
|
urlMapping?: UrlMapping;
|
|
48
|
+
signal?: AbortSignal;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export interface ClientExternalAccountBindingOptions {
|
|
@@ -61,6 +62,7 @@ export interface ClientAutoOptions {
|
|
|
61
62
|
skipChallengeVerification?: boolean;
|
|
62
63
|
challengePriority?: string[];
|
|
63
64
|
preferredChain?: string;
|
|
65
|
+
signal?: AbortSignal;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export class Client {
|