roster-server 2.2.2 → 2.2.5
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 +4 -3
- package/package.json +1 -1
- package/test/acme-dns-01-cli-wrapper.test.js +178 -16
- package/vendor/acme-dns-01-cli-wrapper.js +63 -18
package/README.md
CHANGED
|
@@ -64,14 +64,15 @@ You can serve all subdomains of a domain with a single handler in three ways:
|
|
|
64
64
|
|
|
65
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
66
|
|
|
67
|
-
For fully automatic TXT records with Linode
|
|
67
|
+
For fully automatic TXT records with Linode DNS, set:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
export ROSTER_DNS_PROVIDER=linode
|
|
71
|
-
export LINODE_API_KEY=...
|
|
71
|
+
export LINODE_API_KEY=...
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Then Roster creates/removes `_acme-challenge` TXT records automatically via `api.linode.com`.
|
|
75
|
+
If `LINODE_API_KEY` is present, this mode auto-enables by default for wildcard DNS-01.
|
|
75
76
|
|
|
76
77
|
Override with a custom plugin:
|
|
77
78
|
|
|
@@ -221,7 +222,7 @@ When creating a new `RosterServer` instance, you can pass the following options:
|
|
|
221
222
|
- `email` (string): Your email for Let's Encrypt notifications.
|
|
222
223
|
- `wwwPath` (string): Path to your `www` directory containing your sites.
|
|
223
224
|
- `greenlockStorePath` (string): Directory for Greenlock configuration.
|
|
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
|
|
225
|
+
- `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 Linode DNS API mode by setting `ROSTER_DNS_PROVIDER=linode` and `LINODE_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.
|
|
225
226
|
- `staging` (boolean): Set to `true` to use Let's Encrypt's staging environment (for testing).
|
|
226
227
|
- `local` (boolean): Set to `true` to run in local development mode.
|
|
227
228
|
- `minLocalPort` (number): Minimum port for local mode (default: 4000).
|
package/package.json
CHANGED
|
@@ -16,28 +16,28 @@ function buildChallengeOpts() {
|
|
|
16
16
|
|
|
17
17
|
describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
|
|
18
18
|
const originalFetch = global.fetch;
|
|
19
|
-
const
|
|
19
|
+
const originalLinodeApiKey = process.env.LINODE_API_KEY;
|
|
20
20
|
|
|
21
21
|
afterEach(() => {
|
|
22
22
|
global.fetch = originalFetch;
|
|
23
|
-
if (
|
|
24
|
-
else process.env.
|
|
23
|
+
if (originalLinodeApiKey === undefined) delete process.env.LINODE_API_KEY;
|
|
24
|
+
else process.env.LINODE_API_KEY = originalLinodeApiKey;
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('creates and removes TXT records via Linode API', async () => {
|
|
28
28
|
const calls = [];
|
|
29
29
|
global.fetch = async (url, options = {}) => {
|
|
30
30
|
calls.push({ url, options });
|
|
31
|
-
if (url.endsWith('/domains
|
|
32
|
-
return { ok: true, status: 200, json: async () => ({}) };
|
|
31
|
+
if (url.endsWith('/domains?page_size=500')) {
|
|
32
|
+
return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
|
|
33
33
|
}
|
|
34
|
-
if (url.endsWith('/domains/
|
|
34
|
+
if (url.endsWith('/domains/42/records?page_size=500')) {
|
|
35
35
|
return { ok: true, status: 200, json: async () => ({ data: [] }) };
|
|
36
36
|
}
|
|
37
|
-
if (url.endsWith('/domains/
|
|
37
|
+
if (url.endsWith('/domains/42/records') && options.method === 'POST') {
|
|
38
38
|
return { ok: true, status: 200, json: async () => ({ id: 321 }) };
|
|
39
39
|
}
|
|
40
|
-
if (url.endsWith('/domains/
|
|
40
|
+
if (url.endsWith('/domains/42/records/321') && options.method === 'DELETE') {
|
|
41
41
|
return { ok: true, status: 204, json: async () => ({}) };
|
|
42
42
|
}
|
|
43
43
|
return { ok: false, status: 404, statusText: 'not mocked', text: async () => 'not mocked' };
|
|
@@ -55,26 +55,26 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
|
|
|
55
55
|
await challenger.set(opts);
|
|
56
56
|
await challenger.remove(opts);
|
|
57
57
|
|
|
58
|
-
assert.ok(calls.some((c) => c.url.endsWith('/domains/
|
|
59
|
-
assert.ok(calls.some((c) => c.url.endsWith('/domains/
|
|
58
|
+
assert.ok(calls.some((c) => c.url.endsWith('/domains/42/records') && c.options.method === 'POST'));
|
|
59
|
+
assert.ok(calls.some((c) => c.url.endsWith('/domains/42/records/321') && c.options.method === 'DELETE'));
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('
|
|
63
|
-
process.env.
|
|
62
|
+
it('uses LINODE_API_KEY from environment', async () => {
|
|
63
|
+
process.env.LINODE_API_KEY = 'fake-linode-key';
|
|
64
64
|
const calls = [];
|
|
65
65
|
global.fetch = async (url, options = {}) => {
|
|
66
66
|
calls.push({ url, options });
|
|
67
|
-
if (url.endsWith('/domains
|
|
68
|
-
return { ok: true, status: 200, json: async () => ({}) };
|
|
67
|
+
if (url.endsWith('/domains?page_size=500')) {
|
|
68
|
+
return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
|
|
69
69
|
}
|
|
70
|
-
if (url.endsWith('/domains/
|
|
70
|
+
if (url.endsWith('/domains/42/records?page_size=500')) {
|
|
71
71
|
return { ok: true, status: 200, json: async () => ({ data: [{ id: 111, type: 'TXT', name: '_acme-challenge', target: 'test-token' }] }) };
|
|
72
72
|
}
|
|
73
73
|
return { ok: true, status: 204, json: async () => ({}) };
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
const challenger = wrapper.create({
|
|
77
|
-
provider: '
|
|
77
|
+
provider: 'linode',
|
|
78
78
|
verifyDnsBeforeContinue: false,
|
|
79
79
|
propagationDelay: 0,
|
|
80
80
|
dryRunDelay: 0
|
|
@@ -85,4 +85,166 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
|
|
|
85
85
|
const authHeader = calls[0].options?.headers?.Authorization || '';
|
|
86
86
|
assert.ok(authHeader.startsWith('Bearer '));
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
it('falls back to parent zone when exact zone does not exist', async () => {
|
|
90
|
+
const calls = [];
|
|
91
|
+
global.fetch = async (url, options = {}) => {
|
|
92
|
+
calls.push({ url, options });
|
|
93
|
+
if (url.endsWith('/domains?page_size=500')) {
|
|
94
|
+
return { ok: true, status: 200, json: async () => ({ data: [{ id: 99, domain: 'example.com' }] }) };
|
|
95
|
+
}
|
|
96
|
+
if (url.endsWith('/domains/99/records?page_size=500')) {
|
|
97
|
+
return { ok: true, status: 200, json: async () => ({ data: [] }) };
|
|
98
|
+
}
|
|
99
|
+
if (url.endsWith('/domains/99/records') && options.method === 'POST') {
|
|
100
|
+
return { ok: true, status: 200, json: async () => ({ id: 654 }) };
|
|
101
|
+
}
|
|
102
|
+
if (url.endsWith('/domains/99/records/654') && options.method === 'DELETE') {
|
|
103
|
+
return { ok: true, status: 204, json: async () => ({}) };
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, status: 404, statusText: 'not mocked', text: async () => 'not mocked' };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const challenger = wrapper.create({
|
|
109
|
+
provider: 'linode',
|
|
110
|
+
linodeApiKey: 'fake-token',
|
|
111
|
+
verifyDnsBeforeContinue: false,
|
|
112
|
+
propagationDelay: 0,
|
|
113
|
+
dryRunDelay: 0
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const opts = {
|
|
117
|
+
challenge: {
|
|
118
|
+
altname: '*.sub.example.com',
|
|
119
|
+
dnsHost: '_acme-challenge.sub.example.com',
|
|
120
|
+
dnsAuthorization: 'fallback-token'
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await challenger.set(opts);
|
|
125
|
+
await challenger.remove(opts);
|
|
126
|
+
|
|
127
|
+
const postCall = calls.find((c) => c.url.endsWith('/domains/99/records') && c.options.method === 'POST');
|
|
128
|
+
assert.ok(postCall, 'expected TXT create call on parent zone');
|
|
129
|
+
const payload = JSON.parse(postCall.options.body);
|
|
130
|
+
assert.strictEqual(payload.name, '_acme-challenge.sub');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('falls back to manual when provider mode has no API key (default)', async () => {
|
|
134
|
+
const prevLinode = process.env.LINODE_API_KEY;
|
|
135
|
+
delete process.env.LINODE_API_KEY;
|
|
136
|
+
global.fetch = async () => ({ ok: true, status: 200, json: async () => ({}) });
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const challenger = wrapper.create({
|
|
140
|
+
provider: 'linode',
|
|
141
|
+
verifyDnsBeforeContinue: false,
|
|
142
|
+
propagationDelay: 0,
|
|
143
|
+
dryRunDelay: 0
|
|
144
|
+
});
|
|
145
|
+
await challenger.set(buildChallengeOpts());
|
|
146
|
+
} finally {
|
|
147
|
+
if (prevLinode === undefined) delete process.env.LINODE_API_KEY;
|
|
148
|
+
else process.env.LINODE_API_KEY = prevLinode;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('throws when provider mode has no API key and strict mode is enabled', async () => {
|
|
153
|
+
const prevLinode = process.env.LINODE_API_KEY;
|
|
154
|
+
delete process.env.LINODE_API_KEY;
|
|
155
|
+
global.fetch = async () => ({ ok: true, status: 200, json: async () => ({}) });
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const challenger = wrapper.create({
|
|
159
|
+
provider: 'linode',
|
|
160
|
+
dnsApiFallbackToManual: false,
|
|
161
|
+
verifyDnsBeforeContinue: false,
|
|
162
|
+
propagationDelay: 0,
|
|
163
|
+
dryRunDelay: 0
|
|
164
|
+
});
|
|
165
|
+
await assert.rejects(
|
|
166
|
+
challenger.set(buildChallengeOpts()),
|
|
167
|
+
/Linode API key not configured/
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
if (prevLinode === undefined) delete process.env.LINODE_API_KEY;
|
|
171
|
+
else process.env.LINODE_API_KEY = prevLinode;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('uses configured TXT TTL when creating Linode records', async () => {
|
|
176
|
+
const calls = [];
|
|
177
|
+
global.fetch = async (url, options = {}) => {
|
|
178
|
+
calls.push({ url, options });
|
|
179
|
+
if (url.endsWith('/domains?page_size=500')) {
|
|
180
|
+
return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
|
|
181
|
+
}
|
|
182
|
+
if (url.endsWith('/domains/42/records?page_size=500')) {
|
|
183
|
+
return { ok: true, status: 200, json: async () => ({ data: [] }) };
|
|
184
|
+
}
|
|
185
|
+
if (url.endsWith('/domains/42/records') && options.method === 'POST') {
|
|
186
|
+
return { ok: true, status: 200, json: async () => ({ id: 222 }) };
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, status: 204, json: async () => ({}) };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const challenger = wrapper.create({
|
|
192
|
+
provider: 'linode',
|
|
193
|
+
linodeApiKey: 'fake-token',
|
|
194
|
+
txtRecordTtl: 300,
|
|
195
|
+
verifyDnsBeforeContinue: false,
|
|
196
|
+
propagationDelay: 0,
|
|
197
|
+
dryRunDelay: 0
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await challenger.set(buildChallengeOpts());
|
|
201
|
+
const postCall = calls.find((c) => c.url.endsWith('/domains/42/records') && c.options.method === 'POST');
|
|
202
|
+
assert.ok(postCall, 'expected TXT create call');
|
|
203
|
+
const payload = JSON.parse(postCall.options.body);
|
|
204
|
+
assert.strictEqual(payload.ttl_sec, 300);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('auto-enables Linode provider when API key is present', async () => {
|
|
208
|
+
process.env.LINODE_API_KEY = 'fake-token';
|
|
209
|
+
const calls = [];
|
|
210
|
+
global.fetch = async (url, options = {}) => {
|
|
211
|
+
calls.push({ url, options });
|
|
212
|
+
if (url.endsWith('/domains?page_size=500')) {
|
|
213
|
+
return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
|
|
214
|
+
}
|
|
215
|
+
if (url.endsWith('/domains/42/records?page_size=500')) {
|
|
216
|
+
return { ok: true, status: 200, json: async () => ({ data: [{ id: 111, type: 'TXT', name: '_acme-challenge', target: 'test-token' }] }) };
|
|
217
|
+
}
|
|
218
|
+
return { ok: true, status: 204, json: async () => ({}) };
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const challenger = wrapper.create({
|
|
222
|
+
verifyDnsBeforeContinue: false,
|
|
223
|
+
propagationDelay: 0,
|
|
224
|
+
dryRunDelay: 0
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await challenger.set(buildChallengeOpts());
|
|
228
|
+
assert.ok(calls.length >= 2);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('falls back to manual flow when Linode API fails', async () => {
|
|
232
|
+
let fetchCalls = 0;
|
|
233
|
+
global.fetch = async () => {
|
|
234
|
+
fetchCalls += 1;
|
|
235
|
+
return { ok: false, status: 404, statusText: 'Not Found', text: async () => 'not found' };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const challenger = wrapper.create({
|
|
239
|
+
provider: 'linode',
|
|
240
|
+
linodeApiKey: 'fake-token',
|
|
241
|
+
dnsApiFallbackToManual: true,
|
|
242
|
+
verifyDnsBeforeContinue: false,
|
|
243
|
+
propagationDelay: 0,
|
|
244
|
+
dryRunDelay: 0
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await challenger.set(buildChallengeOpts());
|
|
248
|
+
assert.ok(fetchCalls > 0);
|
|
249
|
+
});
|
|
88
250
|
});
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
const legacyCli = require('acme-dns-01-cli');
|
|
4
4
|
const log = require('lemonlog')('acme-dns-01');
|
|
5
5
|
const dns = require('node:dns').promises;
|
|
6
|
+
let envFileLoadAttempted = false;
|
|
7
|
+
|
|
8
|
+
function loadEnvFileSafely() {
|
|
9
|
+
if (envFileLoadAttempted) return;
|
|
10
|
+
envFileLoadAttempted = true;
|
|
11
|
+
try {
|
|
12
|
+
if (typeof process.loadEnvFile === 'function') {
|
|
13
|
+
process.loadEnvFile();
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
// Ignore missing .env or unsupported runtime behavior.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
|
|
7
20
|
function toPromise(fn, context) {
|
|
8
21
|
if (typeof fn !== 'function') {
|
|
@@ -41,6 +54,7 @@ function toPromise(fn, context) {
|
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
module.exports.create = function create(config = {}) {
|
|
57
|
+
loadEnvFileSafely();
|
|
44
58
|
const challenger = legacyCli.create(config);
|
|
45
59
|
const propagationDelay = Number.isFinite(config.propagationDelay)
|
|
46
60
|
? config.propagationDelay
|
|
@@ -110,13 +124,14 @@ module.exports.create = function create(config = {}) {
|
|
|
110
124
|
const configuredProvider = normalizeProvider(
|
|
111
125
|
config.provider
|
|
112
126
|
|| process.env.ROSTER_DNS_PROVIDER
|
|
113
|
-
|| (config.linodeApiKey || process.env.LINODE_API_KEY
|
|
127
|
+
|| (config.linodeApiKey || process.env.LINODE_API_KEY ? 'linode' : '')
|
|
114
128
|
);
|
|
115
|
-
const isLinodeProvider =
|
|
129
|
+
const isLinodeProvider = configuredProvider === 'linode';
|
|
130
|
+
const dnsApiFallbackToManual = config.dnsApiFallbackToManual !== undefined
|
|
131
|
+
? parseBool(config.dnsApiFallbackToManual, true)
|
|
132
|
+
: parseBool(process.env.ROSTER_DNS_API_FALLBACK_TO_MANUAL, true);
|
|
116
133
|
const linodeApiKey = config.linodeApiKey
|
|
117
134
|
|| process.env.LINODE_API_KEY
|
|
118
|
-
|| process.env.LINODE_TOKEN
|
|
119
|
-
|| process.env.AKAMAI_API_KEY
|
|
120
135
|
|| '';
|
|
121
136
|
const linodeApiBase = String(config.linodeApiBase || process.env.LINODE_API_BASE_URL || 'https://api.linode.com/v4').replace(/\/+$/, '');
|
|
122
137
|
const txtRecordTtl = Number.isFinite(config.txtRecordTtl) ? Math.max(30, Number(config.txtRecordTtl)) : 60;
|
|
@@ -184,6 +199,7 @@ module.exports.create = function create(config = {}) {
|
|
|
184
199
|
const presentedByHost = new Map();
|
|
185
200
|
const presentedByAltname = new Map();
|
|
186
201
|
const linodeTxtRecordsByHost = new Map();
|
|
202
|
+
const linodeZoneCache = new Map();
|
|
187
203
|
|
|
188
204
|
function buildZoneCandidates({ dnsHost, altname }) {
|
|
189
205
|
const candidates = new Set();
|
|
@@ -218,7 +234,7 @@ module.exports.create = function create(config = {}) {
|
|
|
218
234
|
async function linodeRequest(pathname, method = 'GET', body) {
|
|
219
235
|
const apiKey = String(linodeApiKey || '').trim();
|
|
220
236
|
if (!apiKey) {
|
|
221
|
-
throw new Error('Linode API key not configured. Set LINODE_API_KEY
|
|
237
|
+
throw new Error('Linode API key not configured. Set LINODE_API_KEY.');
|
|
222
238
|
}
|
|
223
239
|
if (typeof fetch !== 'function') {
|
|
224
240
|
throw new Error('Global fetch is unavailable in this runtime; cannot call Linode DNS API.');
|
|
@@ -245,22 +261,40 @@ module.exports.create = function create(config = {}) {
|
|
|
245
261
|
return response.json();
|
|
246
262
|
}
|
|
247
263
|
|
|
264
|
+
async function resolveLinodeZone(zone) {
|
|
265
|
+
const normalizedZone = String(zone || '').trim().toLowerCase();
|
|
266
|
+
if (!normalizedZone) return null;
|
|
267
|
+
if (linodeZoneCache.has(normalizedZone)) return linodeZoneCache.get(normalizedZone);
|
|
268
|
+
|
|
269
|
+
const domainsResult = await linodeRequest('/domains?page_size=500', 'GET');
|
|
270
|
+
const domains = Array.isArray(domainsResult?.data) ? domainsResult.data : [];
|
|
271
|
+
const matched = domains.find((entry) => String(entry?.domain || '').trim().toLowerCase() === normalizedZone);
|
|
272
|
+
if (!matched?.id) return null;
|
|
273
|
+
|
|
274
|
+
const zoneInfo = { id: matched.id, domain: String(matched.domain || normalizedZone) };
|
|
275
|
+
linodeZoneCache.set(normalizedZone, zoneInfo);
|
|
276
|
+
return zoneInfo;
|
|
277
|
+
}
|
|
278
|
+
|
|
248
279
|
async function linodeUpsertTxtRecord(dnsHost, dnsAuthorization, altname) {
|
|
249
280
|
const zoneCandidates = buildZoneCandidates({ dnsHost, altname });
|
|
250
281
|
let lastError = null;
|
|
251
282
|
|
|
252
283
|
for (const zone of zoneCandidates) {
|
|
284
|
+
let zoneInfo = null;
|
|
253
285
|
try {
|
|
254
|
-
await
|
|
286
|
+
zoneInfo = await resolveLinodeZone(zone);
|
|
255
287
|
} catch (error) {
|
|
256
288
|
lastError = error;
|
|
257
289
|
continue;
|
|
258
290
|
}
|
|
291
|
+
if (!zoneInfo?.id) continue;
|
|
259
292
|
|
|
260
293
|
const recordName = linodeRecordNameForHost(dnsHost, zone);
|
|
261
294
|
if (!recordName && dnsHost !== zone) continue;
|
|
262
295
|
|
|
263
|
-
const
|
|
296
|
+
const zoneId = zoneInfo.id;
|
|
297
|
+
const recordsResult = await linodeRequest(`/domains/${zoneId}/records?page_size=500`, 'GET');
|
|
264
298
|
const existing = Array.isArray(recordsResult?.data) ? recordsResult.data : [];
|
|
265
299
|
const sameRecord = existing.find((record) =>
|
|
266
300
|
record?.type === 'TXT'
|
|
@@ -269,11 +303,11 @@ module.exports.create = function create(config = {}) {
|
|
|
269
303
|
);
|
|
270
304
|
|
|
271
305
|
if (sameRecord && sameRecord.id) {
|
|
272
|
-
linodeTxtRecordsByHost.set(dnsHost, { zone, id: sameRecord.id });
|
|
273
|
-
return { zone, id: sameRecord.id, reused: true };
|
|
306
|
+
linodeTxtRecordsByHost.set(dnsHost, { zone, zoneId, id: sameRecord.id });
|
|
307
|
+
return { zone, zoneId, id: sameRecord.id, reused: true };
|
|
274
308
|
}
|
|
275
309
|
|
|
276
|
-
const created = await linodeRequest(`/domains/${
|
|
310
|
+
const created = await linodeRequest(`/domains/${zoneId}/records`, 'POST', {
|
|
277
311
|
type: 'TXT',
|
|
278
312
|
name: recordName,
|
|
279
313
|
target: dnsAuthorization,
|
|
@@ -281,8 +315,8 @@ module.exports.create = function create(config = {}) {
|
|
|
281
315
|
});
|
|
282
316
|
|
|
283
317
|
if (created?.id) {
|
|
284
|
-
linodeTxtRecordsByHost.set(dnsHost, { zone, id: created.id });
|
|
285
|
-
return { zone, id: created.id, reused: false };
|
|
318
|
+
linodeTxtRecordsByHost.set(dnsHost, { zone, zoneId, id: created.id });
|
|
319
|
+
return { zone, zoneId, id: created.id, reused: false };
|
|
286
320
|
}
|
|
287
321
|
}
|
|
288
322
|
|
|
@@ -292,9 +326,10 @@ module.exports.create = function create(config = {}) {
|
|
|
292
326
|
|
|
293
327
|
async function linodeRemoveTxtRecord(dnsHost) {
|
|
294
328
|
const stored = linodeTxtRecordsByHost.get(dnsHost);
|
|
295
|
-
if (!stored?.
|
|
296
|
-
await linodeRequest(`/domains/${
|
|
329
|
+
if (!stored?.zoneId || !stored?.id) return false;
|
|
330
|
+
await linodeRequest(`/domains/${stored.zoneId}/records/${stored.id}`, 'DELETE');
|
|
297
331
|
linodeTxtRecordsByHost.delete(dnsHost);
|
|
332
|
+
return true;
|
|
298
333
|
}
|
|
299
334
|
|
|
300
335
|
async function setChallenge(opts) {
|
|
@@ -319,8 +354,16 @@ module.exports.create = function create(config = {}) {
|
|
|
319
354
|
(result?.zone ? ` (zone ${result.zone})` : '')
|
|
320
355
|
);
|
|
321
356
|
} catch (error) {
|
|
322
|
-
|
|
323
|
-
|
|
357
|
+
const errorMsg = error?.message || error;
|
|
358
|
+
if (dnsApiFallbackToManual) {
|
|
359
|
+
log.warn(
|
|
360
|
+
`Linode DNS API failed for ${dnsHost}: ${errorMsg}. ` +
|
|
361
|
+
'Falling back to manual/legacy DNS flow for this challenge.'
|
|
362
|
+
);
|
|
363
|
+
} else {
|
|
364
|
+
log.error(`Failed to create Linode DNS TXT for ${dnsHost}: ${errorMsg}`);
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
324
367
|
}
|
|
325
368
|
}
|
|
326
369
|
const isDryRunChallenge = dnsHost.includes('_greenlock-dryrun-');
|
|
@@ -404,8 +447,10 @@ module.exports.create = function create(config = {}) {
|
|
|
404
447
|
|
|
405
448
|
if (isLinodeProvider && dnsHost) {
|
|
406
449
|
try {
|
|
407
|
-
await linodeRemoveTxtRecord(dnsHost);
|
|
408
|
-
|
|
450
|
+
const removed = await linodeRemoveTxtRecord(dnsHost);
|
|
451
|
+
if (removed) {
|
|
452
|
+
log.info(`Linode DNS TXT removed for ${dnsHost}`);
|
|
453
|
+
}
|
|
409
454
|
return null;
|
|
410
455
|
} catch (error) {
|
|
411
456
|
log.warn(`Failed to remove Linode DNS TXT for ${dnsHost}: ${error?.message || error}`);
|