roster-server 2.2.4 → 2.2.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -28,16 +28,16 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
28
28
  const calls = [];
29
29
  global.fetch = async (url, options = {}) => {
30
30
  calls.push({ url, options });
31
- if (url.endsWith('/domains/example.com')) {
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/example.com/records?page_size=500')) {
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/example.com/records') && options.method === 'POST') {
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/example.com/records/321') && options.method === 'DELETE') {
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,8 +55,8 @@ 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/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'));
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
62
  it('uses LINODE_API_KEY from environment', async () => {
@@ -64,10 +64,10 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
64
64
  const calls = [];
65
65
  global.fetch = async (url, options = {}) => {
66
66
  calls.push({ url, options });
67
- if (url.endsWith('/domains/example.com')) {
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/example.com/records?page_size=500')) {
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 () => ({}) };
@@ -90,19 +90,16 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
90
90
  const calls = [];
91
91
  global.fetch = async (url, options = {}) => {
92
92
  calls.push({ url, options });
93
- if (url.endsWith('/domains/sub.example.com')) {
94
- return { ok: false, status: 404, statusText: 'Not Found', text: async () => 'not found' };
93
+ if (url.endsWith('/domains?page_size=500')) {
94
+ return { ok: true, status: 200, json: async () => ({ data: [{ id: 99, domain: 'example.com' }] }) };
95
95
  }
96
- if (url.endsWith('/domains/example.com')) {
97
- return { ok: true, status: 200, json: async () => ({}) };
98
- }
99
- if (url.endsWith('/domains/example.com/records?page_size=500')) {
96
+ if (url.endsWith('/domains/99/records?page_size=500')) {
100
97
  return { ok: true, status: 200, json: async () => ({ data: [] }) };
101
98
  }
102
- if (url.endsWith('/domains/example.com/records') && options.method === 'POST') {
99
+ if (url.endsWith('/domains/99/records') && options.method === 'POST') {
103
100
  return { ok: true, status: 200, json: async () => ({ id: 654 }) };
104
101
  }
105
- if (url.endsWith('/domains/example.com/records/654') && options.method === 'DELETE') {
102
+ if (url.endsWith('/domains/99/records/654') && options.method === 'DELETE') {
106
103
  return { ok: true, status: 204, json: async () => ({}) };
107
104
  }
108
105
  return { ok: false, status: 404, statusText: 'not mocked', text: async () => 'not mocked' };
@@ -127,12 +124,57 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
127
124
  await challenger.set(opts);
128
125
  await challenger.remove(opts);
129
126
 
130
- const postCall = calls.find((c) => c.url.endsWith('/domains/example.com/records') && c.options.method === 'POST');
127
+ const postCall = calls.find((c) => c.url.endsWith('/domains/99/records') && c.options.method === 'POST');
131
128
  assert.ok(postCall, 'expected TXT create call on parent zone');
132
129
  const payload = JSON.parse(postCall.options.body);
133
130
  assert.strictEqual(payload.name, '_acme-challenge.sub');
134
131
  });
135
132
 
133
+ it('prefers apex zone over www zone for www challenges', async () => {
134
+ const calls = [];
135
+ global.fetch = async (url, options = {}) => {
136
+ calls.push({ url, options });
137
+ if (url.endsWith('/domains?page_size=500')) {
138
+ return {
139
+ ok: true,
140
+ status: 200,
141
+ json: async () => ({
142
+ data: [
143
+ { id: 777, domain: 'www.tagnu.com' },
144
+ { id: 888, domain: 'tagnu.com' }
145
+ ]
146
+ })
147
+ };
148
+ }
149
+ if (url.endsWith('/domains/888/records?page_size=500')) {
150
+ return { ok: true, status: 200, json: async () => ({ data: [] }) };
151
+ }
152
+ if (url.endsWith('/domains/888/records') && options.method === 'POST') {
153
+ return { ok: true, status: 200, json: async () => ({ id: 333 }) };
154
+ }
155
+ return { ok: true, status: 204, json: async () => ({}) };
156
+ };
157
+
158
+ const challenger = wrapper.create({
159
+ provider: 'linode',
160
+ linodeApiKey: 'fake-token',
161
+ verifyDnsBeforeContinue: false,
162
+ propagationDelay: 0,
163
+ dryRunDelay: 0
164
+ });
165
+
166
+ await challenger.set({
167
+ challenge: {
168
+ altname: 'www.tagnu.com',
169
+ dnsHost: '_greenlock-dryrun-abc.www.tagnu.com',
170
+ dnsAuthorization: 'www-token'
171
+ }
172
+ });
173
+
174
+ assert.ok(calls.some((c) => c.url.endsWith('/domains/888/records') && c.options.method === 'POST'));
175
+ assert.ok(!calls.some((c) => c.url.endsWith('/domains/777/records') && c.options.method === 'POST'));
176
+ });
177
+
136
178
  it('falls back to manual when provider mode has no API key (default)', async () => {
137
179
  const prevLinode = process.env.LINODE_API_KEY;
138
180
  delete process.env.LINODE_API_KEY;
@@ -179,13 +221,13 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
179
221
  const calls = [];
180
222
  global.fetch = async (url, options = {}) => {
181
223
  calls.push({ url, options });
182
- if (url.endsWith('/domains/example.com')) {
183
- return { ok: true, status: 200, json: async () => ({}) };
224
+ if (url.endsWith('/domains?page_size=500')) {
225
+ return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
184
226
  }
185
- if (url.endsWith('/domains/example.com/records?page_size=500')) {
227
+ if (url.endsWith('/domains/42/records?page_size=500')) {
186
228
  return { ok: true, status: 200, json: async () => ({ data: [] }) };
187
229
  }
188
- if (url.endsWith('/domains/example.com/records') && options.method === 'POST') {
230
+ if (url.endsWith('/domains/42/records') && options.method === 'POST') {
189
231
  return { ok: true, status: 200, json: async () => ({ id: 222 }) };
190
232
  }
191
233
  return { ok: true, status: 204, json: async () => ({}) };
@@ -201,7 +243,7 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
201
243
  });
202
244
 
203
245
  await challenger.set(buildChallengeOpts());
204
- const postCall = calls.find((c) => c.url.endsWith('/domains/example.com/records') && c.options.method === 'POST');
246
+ const postCall = calls.find((c) => c.url.endsWith('/domains/42/records') && c.options.method === 'POST');
205
247
  assert.ok(postCall, 'expected TXT create call');
206
248
  const payload = JSON.parse(postCall.options.body);
207
249
  assert.strictEqual(payload.ttl_sec, 300);
@@ -212,10 +254,10 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
212
254
  const calls = [];
213
255
  global.fetch = async (url, options = {}) => {
214
256
  calls.push({ url, options });
215
- if (url.endsWith('/domains/example.com')) {
216
- return { ok: true, status: 200, json: async () => ({}) };
257
+ if (url.endsWith('/domains?page_size=500')) {
258
+ return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
217
259
  }
218
- if (url.endsWith('/domains/example.com/records?page_size=500')) {
260
+ if (url.endsWith('/domains/42/records?page_size=500')) {
219
261
  return { ok: true, status: 200, json: async () => ({ data: [{ id: 111, type: 'TXT', name: '_acme-challenge', target: 'test-token' }] }) };
220
262
  }
221
263
  return { ok: true, status: 204, json: async () => ({}) };
@@ -199,6 +199,7 @@ module.exports.create = function create(config = {}) {
199
199
  const presentedByHost = new Map();
200
200
  const presentedByAltname = new Map();
201
201
  const linodeTxtRecordsByHost = new Map();
202
+ const linodeZoneCache = new Map();
202
203
 
203
204
  function buildZoneCandidates({ dnsHost, altname }) {
204
205
  const candidates = new Set();
@@ -211,6 +212,14 @@ module.exports.create = function create(config = {}) {
211
212
  }
212
213
  };
213
214
 
215
+ // Prefer apex zone when validating www.<domain> challenges so records are
216
+ // created in the commonly delegated parent zone (e.g. tagnu.com).
217
+ if (altname) {
218
+ const normalizedAltname = String(altname).replace(/^\*\./, '').replace(/\.$/, '').toLowerCase();
219
+ if (normalizedAltname.startsWith('www.')) {
220
+ add(normalizedAltname.slice(4));
221
+ }
222
+ }
214
223
  if (dnsHost) {
215
224
  const normalizedDnsHost = String(dnsHost).replace(/^_acme-challenge\./, '').replace(/^_greenlock-[^.]+\./, '');
216
225
  add(normalizedDnsHost);
@@ -260,22 +269,40 @@ module.exports.create = function create(config = {}) {
260
269
  return response.json();
261
270
  }
262
271
 
272
+ async function resolveLinodeZone(zone) {
273
+ const normalizedZone = String(zone || '').trim().toLowerCase();
274
+ if (!normalizedZone) return null;
275
+ if (linodeZoneCache.has(normalizedZone)) return linodeZoneCache.get(normalizedZone);
276
+
277
+ const domainsResult = await linodeRequest('/domains?page_size=500', 'GET');
278
+ const domains = Array.isArray(domainsResult?.data) ? domainsResult.data : [];
279
+ const matched = domains.find((entry) => String(entry?.domain || '').trim().toLowerCase() === normalizedZone);
280
+ if (!matched?.id) return null;
281
+
282
+ const zoneInfo = { id: matched.id, domain: String(matched.domain || normalizedZone) };
283
+ linodeZoneCache.set(normalizedZone, zoneInfo);
284
+ return zoneInfo;
285
+ }
286
+
263
287
  async function linodeUpsertTxtRecord(dnsHost, dnsAuthorization, altname) {
264
288
  const zoneCandidates = buildZoneCandidates({ dnsHost, altname });
265
289
  let lastError = null;
266
290
 
267
291
  for (const zone of zoneCandidates) {
292
+ let zoneInfo = null;
268
293
  try {
269
- await linodeRequest(`/domains/${encodeURIComponent(zone)}`, 'GET');
294
+ zoneInfo = await resolveLinodeZone(zone);
270
295
  } catch (error) {
271
296
  lastError = error;
272
297
  continue;
273
298
  }
299
+ if (!zoneInfo?.id) continue;
274
300
 
275
301
  const recordName = linodeRecordNameForHost(dnsHost, zone);
276
302
  if (!recordName && dnsHost !== zone) continue;
277
303
 
278
- const recordsResult = await linodeRequest(`/domains/${encodeURIComponent(zone)}/records?page_size=500`, 'GET');
304
+ const zoneId = zoneInfo.id;
305
+ const recordsResult = await linodeRequest(`/domains/${zoneId}/records?page_size=500`, 'GET');
279
306
  const existing = Array.isArray(recordsResult?.data) ? recordsResult.data : [];
280
307
  const sameRecord = existing.find((record) =>
281
308
  record?.type === 'TXT'
@@ -284,11 +311,11 @@ module.exports.create = function create(config = {}) {
284
311
  );
285
312
 
286
313
  if (sameRecord && sameRecord.id) {
287
- linodeTxtRecordsByHost.set(dnsHost, { zone, id: sameRecord.id });
288
- return { zone, id: sameRecord.id, reused: true };
314
+ linodeTxtRecordsByHost.set(dnsHost, { zone, zoneId, id: sameRecord.id });
315
+ return { zone, zoneId, id: sameRecord.id, reused: true };
289
316
  }
290
317
 
291
- const created = await linodeRequest(`/domains/${encodeURIComponent(zone)}/records`, 'POST', {
318
+ const created = await linodeRequest(`/domains/${zoneId}/records`, 'POST', {
292
319
  type: 'TXT',
293
320
  name: recordName,
294
321
  target: dnsAuthorization,
@@ -296,8 +323,8 @@ module.exports.create = function create(config = {}) {
296
323
  });
297
324
 
298
325
  if (created?.id) {
299
- linodeTxtRecordsByHost.set(dnsHost, { zone, id: created.id });
300
- return { zone, id: created.id, reused: false };
326
+ linodeTxtRecordsByHost.set(dnsHost, { zone, zoneId, id: created.id });
327
+ return { zone, zoneId, id: created.id, reused: false };
301
328
  }
302
329
  }
303
330
 
@@ -307,8 +334,8 @@ module.exports.create = function create(config = {}) {
307
334
 
308
335
  async function linodeRemoveTxtRecord(dnsHost) {
309
336
  const stored = linodeTxtRecordsByHost.get(dnsHost);
310
- if (!stored?.zone || !stored?.id) return false;
311
- await linodeRequest(`/domains/${encodeURIComponent(stored.zone)}/records/${stored.id}`, 'DELETE');
337
+ if (!stored?.zoneId || !stored?.id) return false;
338
+ await linodeRequest(`/domains/${stored.zoneId}/records/${stored.id}`, 'DELETE');
312
339
  linodeTxtRecordsByHost.delete(dnsHost);
313
340
  return true;
314
341
  }