roster-server 2.2.0 → 2.2.2
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/README.md +13 -2
- package/index.js +26 -2
- package/package.json +1 -1
- package/tasks/lessons.md +3 -0
- package/test/acme-dns-01-cli-wrapper.test.js +88 -0
- package/vendor/acme-dns-01-cli-wrapper.js +166 -3
- package/.claude/settings.local.json +0 -11
package/README.md
CHANGED
|
@@ -62,7 +62,18 @@ You can serve all subdomains of a domain with a single handler in three ways:
|
|
|
62
62
|
2. **Register (default port)**: `roster.register('*.example.com', handler)` for the default HTTPS port.
|
|
63
63
|
3. **Register (custom port)**: `roster.register('*.example.com:8080', handler)` for a specific port.
|
|
64
64
|
|
|
65
|
-
Wildcard SSL certificates require **DNS-01** validation (Let's Encrypt does not support HTTP-01 for wildcards). By default Roster uses `acme-dns-01-cli` through an internal wrapper (adds `propagationDelay` and modern plugin signatures).
|
|
65
|
+
Wildcard SSL certificates require **DNS-01** validation (Let's Encrypt does not support HTTP-01 for wildcards). By default Roster uses `acme-dns-01-cli` through an internal wrapper (adds `propagationDelay` and modern plugin signatures).
|
|
66
|
+
|
|
67
|
+
For fully automatic TXT records with Linode/Akamai DNS Manager, set:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export ROSTER_DNS_PROVIDER=linode
|
|
71
|
+
export LINODE_API_KEY=... # or LINODE_TOKEN / AKAMAI_API_KEY
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then Roster creates/removes `_acme-challenge` TXT records automatically via `api.linode.com`.
|
|
75
|
+
|
|
76
|
+
Override with a custom plugin:
|
|
66
77
|
|
|
67
78
|
```javascript
|
|
68
79
|
import Roster from 'roster-server';
|
|
@@ -210,7 +221,7 @@ When creating a new `RosterServer` instance, you can pass the following options:
|
|
|
210
221
|
- `email` (string): Your email for Let's Encrypt notifications.
|
|
211
222
|
- `wwwPath` (string): Path to your `www` directory containing your sites.
|
|
212
223
|
- `greenlockStorePath` (string): Directory for Greenlock configuration.
|
|
213
|
-
- `dnsChallenge` (object|false): Optional override for wildcard DNS-01 challenge config. Default is
|
|
224
|
+
- `dnsChallenge` (object|false): Optional override for wildcard DNS-01 challenge config. Default is `acme-dns-01-cli` wrapper with `propagationDelay: 120000`, `autoContinue: false`, and `dryRunDelay: 120000`. Manual mode still works, but you can enable automatic DNS API mode for Linode/Akamai by setting `ROSTER_DNS_PROVIDER=linode` (or `akamai`) and `LINODE_API_KEY` (aliases: `LINODE_TOKEN`, `AKAMAI_API_KEY`). In automatic mode, Roster creates/removes TXT records itself and still polls public resolvers every 15s before continuing. Set `false` to disable DNS challenge. You can pass `{ module: '...', propagationDelay: 180000 }` to tune DNS wait time (ms). For Greenlock dry-runs (`_greenlock-dryrun-*`), delay defaults to `dryRunDelay` (same as `propagationDelay` unless overridden with `dnsChallenge.dryRunDelay` or env `ROSTER_DNS_DRYRUN_DELAY_MS`). When wildcard sites are present, Roster creates a separate wildcard certificate (`*.example.com`) that uses `dns-01`, while apex/www stay on the regular certificate flow (typically `http-01`), reducing manual TXT records.
|
|
214
225
|
- `staging` (boolean): Set to `true` to use Let's Encrypt's staging environment (for testing).
|
|
215
226
|
- `local` (boolean): Set to `true` to run in local development mode.
|
|
216
227
|
- `minLocalPort` (number): Minimum port for local mode (default: 4000).
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const tls = require('tls');
|
|
6
|
+
const crypto = require('crypto');
|
|
6
7
|
const { EventEmitter } = require('events');
|
|
7
8
|
const Greenlock = require('./vendor/greenlock-express/greenlock-express.js');
|
|
8
9
|
const GreenlockShim = require('./vendor/greenlock-express/greenlock-shim.js');
|
|
@@ -89,6 +90,16 @@ function buildCertLookupCandidates(servername) {
|
|
|
89
90
|
return candidates;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
function certCoversName(certPem, name) {
|
|
94
|
+
try {
|
|
95
|
+
const x509 = new crypto.X509Certificate(certPem);
|
|
96
|
+
const san = (x509.subjectAltName || '').toLowerCase();
|
|
97
|
+
return san.split(',').some(entry => entry.trim() === `dns:${name.toLowerCase()}`);
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
function parseBooleanFlag(value, fallback = false) {
|
|
93
104
|
if (value === undefined || value === null || value === '') return fallback;
|
|
94
105
|
const normalized = String(value).trim().toLowerCase();
|
|
@@ -909,10 +920,22 @@ class Roster {
|
|
|
909
920
|
};
|
|
910
921
|
const ensureBunDefaultPems = async (primaryDomain) => {
|
|
911
922
|
let pems = await issueAndReloadPemsForServername(primaryDomain);
|
|
923
|
+
|
|
924
|
+
const needsWildcard = this.combineWildcardCerts
|
|
925
|
+
&& this.wildcardZones.has(primaryDomain)
|
|
926
|
+
&& this.dnsChallenge;
|
|
927
|
+
|
|
928
|
+
if (pems && needsWildcard && !certCoversName(pems.cert, `*.${primaryDomain}`)) {
|
|
929
|
+
log.warn(`⚠️ Existing cert for ${primaryDomain} lacks *.${primaryDomain} SAN — clearing stale cert for combined re-issuance`);
|
|
930
|
+
const certDir = path.join(greenlockStorePath, 'live', primaryDomain);
|
|
931
|
+
try { fs.rmSync(certDir, { recursive: true, force: true }); } catch {}
|
|
932
|
+
pems = null;
|
|
933
|
+
}
|
|
934
|
+
|
|
912
935
|
if (pems) return pems;
|
|
913
936
|
|
|
914
937
|
const certSubject = primaryDomain.startsWith('*.') ? wildcardRoot(primaryDomain) : primaryDomain;
|
|
915
|
-
log.warn(`⚠️ Bun
|
|
938
|
+
log.warn(`⚠️ Bun: requesting ${needsWildcard ? 'combined wildcard' : ''} certificate for ${certSubject} via Greenlock before HTTPS bind`);
|
|
916
939
|
try {
|
|
917
940
|
await greenlockRuntime.get({ servername: certSubject });
|
|
918
941
|
} catch (error) {
|
|
@@ -1074,4 +1097,5 @@ module.exports.isBunRuntime = isBunRuntime;
|
|
|
1074
1097
|
module.exports.wildcardRoot = wildcardRoot;
|
|
1075
1098
|
module.exports.hostMatchesWildcard = hostMatchesWildcard;
|
|
1076
1099
|
module.exports.wildcardSubjectForHost = wildcardSubjectForHost;
|
|
1077
|
-
module.exports.buildCertLookupCandidates = buildCertLookupCandidates;
|
|
1100
|
+
module.exports.buildCertLookupCandidates = buildCertLookupCandidates;
|
|
1101
|
+
module.exports.certCoversName = certCoversName;
|
package/package.json
CHANGED
package/tasks/lessons.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, afterEach } = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
const wrapper = require('../vendor/acme-dns-01-cli-wrapper.js');
|
|
6
|
+
|
|
7
|
+
function buildChallengeOpts() {
|
|
8
|
+
return {
|
|
9
|
+
challenge: {
|
|
10
|
+
altname: '*.example.com',
|
|
11
|
+
dnsHost: '_acme-challenge.example.com',
|
|
12
|
+
dnsAuthorization: 'test-token'
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
|
|
18
|
+
const originalFetch = global.fetch;
|
|
19
|
+
const originalAkamaiApiKey = process.env.AKAMAI_API_KEY;
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
global.fetch = originalFetch;
|
|
23
|
+
if (originalAkamaiApiKey === undefined) delete process.env.AKAMAI_API_KEY;
|
|
24
|
+
else process.env.AKAMAI_API_KEY = originalAkamaiApiKey;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('creates and removes TXT records via Linode API', async () => {
|
|
28
|
+
const calls = [];
|
|
29
|
+
global.fetch = async (url, options = {}) => {
|
|
30
|
+
calls.push({ url, options });
|
|
31
|
+
if (url.endsWith('/domains/example.com')) {
|
|
32
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
33
|
+
}
|
|
34
|
+
if (url.endsWith('/domains/example.com/records?page_size=500')) {
|
|
35
|
+
return { ok: true, status: 200, json: async () => ({ data: [] }) };
|
|
36
|
+
}
|
|
37
|
+
if (url.endsWith('/domains/example.com/records') && options.method === 'POST') {
|
|
38
|
+
return { ok: true, status: 200, json: async () => ({ id: 321 }) };
|
|
39
|
+
}
|
|
40
|
+
if (url.endsWith('/domains/example.com/records/321') && options.method === 'DELETE') {
|
|
41
|
+
return { ok: true, status: 204, json: async () => ({}) };
|
|
42
|
+
}
|
|
43
|
+
return { ok: false, status: 404, statusText: 'not mocked', text: async () => 'not mocked' };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const challenger = wrapper.create({
|
|
47
|
+
provider: 'linode',
|
|
48
|
+
linodeApiKey: 'fake-token',
|
|
49
|
+
verifyDnsBeforeContinue: false,
|
|
50
|
+
propagationDelay: 0,
|
|
51
|
+
dryRunDelay: 0
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const opts = buildChallengeOpts();
|
|
55
|
+
await challenger.set(opts);
|
|
56
|
+
await challenger.remove(opts);
|
|
57
|
+
|
|
58
|
+
assert.ok(calls.some((c) => c.url.endsWith('/domains/example.com/records') && c.options.method === 'POST'));
|
|
59
|
+
assert.ok(calls.some((c) => c.url.endsWith('/domains/example.com/records/321') && c.options.method === 'DELETE'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('accepts AKAMAI_API_KEY as Linode token alias', async () => {
|
|
63
|
+
process.env.AKAMAI_API_KEY = 'fake-ak-key';
|
|
64
|
+
const calls = [];
|
|
65
|
+
global.fetch = async (url, options = {}) => {
|
|
66
|
+
calls.push({ url, options });
|
|
67
|
+
if (url.endsWith('/domains/example.com')) {
|
|
68
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
69
|
+
}
|
|
70
|
+
if (url.endsWith('/domains/example.com/records?page_size=500')) {
|
|
71
|
+
return { ok: true, status: 200, json: async () => ({ data: [{ id: 111, type: 'TXT', name: '_acme-challenge', target: 'test-token' }] }) };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, status: 204, json: async () => ({}) };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const challenger = wrapper.create({
|
|
77
|
+
provider: 'akamai',
|
|
78
|
+
verifyDnsBeforeContinue: false,
|
|
79
|
+
propagationDelay: 0,
|
|
80
|
+
dryRunDelay: 0
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await challenger.set(buildChallengeOpts());
|
|
84
|
+
assert.ok(calls.length >= 2);
|
|
85
|
+
const authHeader = calls[0].options?.headers?.Authorization || '';
|
|
86
|
+
assert.ok(authHeader.startsWith('Bearer '));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -106,6 +106,20 @@ module.exports.create = function create(config = {}) {
|
|
|
106
106
|
resolver.setServers([server]);
|
|
107
107
|
return { server, resolver };
|
|
108
108
|
});
|
|
109
|
+
const normalizeProvider = (value) => String(value || '').trim().toLowerCase();
|
|
110
|
+
const configuredProvider = normalizeProvider(
|
|
111
|
+
config.provider
|
|
112
|
+
|| process.env.ROSTER_DNS_PROVIDER
|
|
113
|
+
|| (config.linodeApiKey || process.env.LINODE_API_KEY || process.env.LINODE_TOKEN || process.env.AKAMAI_API_KEY ? 'linode' : '')
|
|
114
|
+
);
|
|
115
|
+
const isLinodeProvider = ['linode', 'akamai', 'linode-akamai', 'akamai-linode'].includes(configuredProvider);
|
|
116
|
+
const linodeApiKey = config.linodeApiKey
|
|
117
|
+
|| process.env.LINODE_API_KEY
|
|
118
|
+
|| process.env.LINODE_TOKEN
|
|
119
|
+
|| process.env.AKAMAI_API_KEY
|
|
120
|
+
|| '';
|
|
121
|
+
const linodeApiBase = String(config.linodeApiBase || process.env.LINODE_API_BASE_URL || 'https://api.linode.com/v4').replace(/\/+$/, '');
|
|
122
|
+
const txtRecordTtl = Number.isFinite(config.txtRecordTtl) ? Math.max(30, Number(config.txtRecordTtl)) : 60;
|
|
109
123
|
|
|
110
124
|
function sleep(ms) {
|
|
111
125
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -169,6 +183,119 @@ module.exports.create = function create(config = {}) {
|
|
|
169
183
|
|
|
170
184
|
const presentedByHost = new Map();
|
|
171
185
|
const presentedByAltname = new Map();
|
|
186
|
+
const linodeTxtRecordsByHost = new Map();
|
|
187
|
+
|
|
188
|
+
function buildZoneCandidates({ dnsHost, altname }) {
|
|
189
|
+
const candidates = new Set();
|
|
190
|
+
const add = (value) => {
|
|
191
|
+
const normalized = String(value || '').replace(/\.$/, '').toLowerCase();
|
|
192
|
+
if (!normalized) return;
|
|
193
|
+
const labels = normalized.split('.').filter(Boolean);
|
|
194
|
+
for (let i = 0; i <= labels.length - 2; i += 1) {
|
|
195
|
+
candidates.add(labels.slice(i).join('.'));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (dnsHost) {
|
|
200
|
+
const normalizedDnsHost = String(dnsHost).replace(/^_acme-challenge\./, '').replace(/^_greenlock-[^.]+\./, '');
|
|
201
|
+
add(normalizedDnsHost);
|
|
202
|
+
}
|
|
203
|
+
if (altname) {
|
|
204
|
+
add(String(altname).replace(/^\*\./, ''));
|
|
205
|
+
}
|
|
206
|
+
return Array.from(candidates);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function linodeRecordNameForHost(dnsHost, zone) {
|
|
210
|
+
const host = String(dnsHost || '').replace(/\.$/, '').toLowerCase();
|
|
211
|
+
const normalizedZone = String(zone || '').replace(/\.$/, '').toLowerCase();
|
|
212
|
+
if (!host || !normalizedZone) return '';
|
|
213
|
+
if (host === normalizedZone) return '';
|
|
214
|
+
if (!host.endsWith(`.${normalizedZone}`)) return '';
|
|
215
|
+
return host.slice(0, host.length - normalizedZone.length - 1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function linodeRequest(pathname, method = 'GET', body) {
|
|
219
|
+
const apiKey = String(linodeApiKey || '').trim();
|
|
220
|
+
if (!apiKey) {
|
|
221
|
+
throw new Error('Linode API key not configured. Set LINODE_API_KEY (or LINODE_TOKEN/AKAMAI_API_KEY).');
|
|
222
|
+
}
|
|
223
|
+
if (typeof fetch !== 'function') {
|
|
224
|
+
throw new Error('Global fetch is unavailable in this runtime; cannot call Linode DNS API.');
|
|
225
|
+
}
|
|
226
|
+
const response = await fetch(`${linodeApiBase}${pathname}`, {
|
|
227
|
+
method,
|
|
228
|
+
headers: {
|
|
229
|
+
Authorization: `Bearer ${apiKey}`,
|
|
230
|
+
'Content-Type': 'application/json'
|
|
231
|
+
},
|
|
232
|
+
...(body ? { body: JSON.stringify(body) } : {})
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
let details = '';
|
|
237
|
+
try {
|
|
238
|
+
details = await response.text();
|
|
239
|
+
} catch {
|
|
240
|
+
details = '';
|
|
241
|
+
}
|
|
242
|
+
throw new Error(`Linode API ${method} ${pathname} failed (${response.status}): ${details || response.statusText}`);
|
|
243
|
+
}
|
|
244
|
+
if (response.status === 204) return null;
|
|
245
|
+
return response.json();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function linodeUpsertTxtRecord(dnsHost, dnsAuthorization, altname) {
|
|
249
|
+
const zoneCandidates = buildZoneCandidates({ dnsHost, altname });
|
|
250
|
+
let lastError = null;
|
|
251
|
+
|
|
252
|
+
for (const zone of zoneCandidates) {
|
|
253
|
+
try {
|
|
254
|
+
await linodeRequest(`/domains/${encodeURIComponent(zone)}`, 'GET');
|
|
255
|
+
} catch (error) {
|
|
256
|
+
lastError = error;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const recordName = linodeRecordNameForHost(dnsHost, zone);
|
|
261
|
+
if (!recordName && dnsHost !== zone) continue;
|
|
262
|
+
|
|
263
|
+
const recordsResult = await linodeRequest(`/domains/${encodeURIComponent(zone)}/records?page_size=500`, 'GET');
|
|
264
|
+
const existing = Array.isArray(recordsResult?.data) ? recordsResult.data : [];
|
|
265
|
+
const sameRecord = existing.find((record) =>
|
|
266
|
+
record?.type === 'TXT'
|
|
267
|
+
&& String(record?.name || '') === String(recordName || '')
|
|
268
|
+
&& String(record?.target || '') === String(dnsAuthorization || '')
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (sameRecord && sameRecord.id) {
|
|
272
|
+
linodeTxtRecordsByHost.set(dnsHost, { zone, id: sameRecord.id });
|
|
273
|
+
return { zone, id: sameRecord.id, reused: true };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const created = await linodeRequest(`/domains/${encodeURIComponent(zone)}/records`, 'POST', {
|
|
277
|
+
type: 'TXT',
|
|
278
|
+
name: recordName,
|
|
279
|
+
target: dnsAuthorization,
|
|
280
|
+
ttl_sec: txtRecordTtl
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (created?.id) {
|
|
284
|
+
linodeTxtRecordsByHost.set(dnsHost, { zone, id: created.id });
|
|
285
|
+
return { zone, id: created.id, reused: false };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (lastError) throw lastError;
|
|
290
|
+
throw new Error(`Unable to map ${dnsHost} to a Linode DNS zone`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function linodeRemoveTxtRecord(dnsHost) {
|
|
294
|
+
const stored = linodeTxtRecordsByHost.get(dnsHost);
|
|
295
|
+
if (!stored?.zone || !stored?.id) return;
|
|
296
|
+
await linodeRequest(`/domains/${encodeURIComponent(stored.zone)}/records/${stored.id}`, 'DELETE');
|
|
297
|
+
linodeTxtRecordsByHost.delete(dnsHost);
|
|
298
|
+
}
|
|
172
299
|
|
|
173
300
|
async function setChallenge(opts) {
|
|
174
301
|
const ch = opts?.challenge || {};
|
|
@@ -184,6 +311,18 @@ module.exports.create = function create(config = {}) {
|
|
|
184
311
|
if (altname && dnsAuth) {
|
|
185
312
|
presentedByAltname.set(altname, { dnsHost, dnsAuthorization: dnsAuth });
|
|
186
313
|
}
|
|
314
|
+
if (isLinodeProvider && dnsHost && dnsAuth) {
|
|
315
|
+
try {
|
|
316
|
+
const result = await linodeUpsertTxtRecord(dnsHost, dnsAuth, altname);
|
|
317
|
+
log.info(
|
|
318
|
+
`Linode DNS TXT ${result?.reused ? 'reused' : 'created'} for ${dnsHost}` +
|
|
319
|
+
(result?.zone ? ` (zone ${result.zone})` : '')
|
|
320
|
+
);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
log.error(`Failed to create Linode DNS TXT for ${dnsHost}: ${error?.message || error}`);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
187
326
|
const isDryRunChallenge = dnsHost.includes('_greenlock-dryrun-');
|
|
188
327
|
const effectiveDelay = isDryRunChallenge
|
|
189
328
|
? Math.max(0, dryRunDelay)
|
|
@@ -218,8 +357,10 @@ module.exports.create = function create(config = {}) {
|
|
|
218
357
|
}
|
|
219
358
|
|
|
220
359
|
log.info(
|
|
221
|
-
|
|
222
|
-
|
|
360
|
+
(isLinodeProvider
|
|
361
|
+
? 'Automatic DNS provider mode detected.'
|
|
362
|
+
: 'Non-interactive mode (or autoContinue) detected. Set the TXT record now.') +
|
|
363
|
+
' Continuing automatically in ' +
|
|
223
364
|
effectiveDelay +
|
|
224
365
|
'ms...'
|
|
225
366
|
);
|
|
@@ -254,10 +395,32 @@ module.exports.create = function create(config = {}) {
|
|
|
254
395
|
};
|
|
255
396
|
}
|
|
256
397
|
|
|
398
|
+
async function removeChallenge(opts) {
|
|
399
|
+
const ch = opts?.challenge || {};
|
|
400
|
+
const altname = String(ch.altname || opts?.altname || '');
|
|
401
|
+
const wildcardZone = altname.startsWith('*.') ? altname.slice(2) : '';
|
|
402
|
+
const dnsHostFromAltname = wildcardZone ? `_acme-challenge.${wildcardZone}` : '';
|
|
403
|
+
const dnsHost = String(ch.dnsHost || opts?.dnsHost || dnsHostFromAltname || '');
|
|
404
|
+
|
|
405
|
+
if (isLinodeProvider && dnsHost) {
|
|
406
|
+
try {
|
|
407
|
+
await linodeRemoveTxtRecord(dnsHost);
|
|
408
|
+
log.info(`Linode DNS TXT removed for ${dnsHost}`);
|
|
409
|
+
return null;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
log.warn(`Failed to remove Linode DNS TXT for ${dnsHost}: ${error?.message || error}`);
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const legacyRemove = toPromise(challenger.remove, challenger);
|
|
417
|
+
return legacyRemove(opts);
|
|
418
|
+
}
|
|
419
|
+
|
|
257
420
|
const wrapped = {
|
|
258
421
|
propagationDelay,
|
|
259
422
|
set: setChallenge,
|
|
260
|
-
remove:
|
|
423
|
+
remove: removeChallenge,
|
|
261
424
|
get: getChallenge,
|
|
262
425
|
zones: async (opts) => {
|
|
263
426
|
const dnsHost =
|