roster-server 2.2.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
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,7 +124,7 @@ 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');
@@ -179,13 +176,13 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
179
176
  const calls = [];
180
177
  global.fetch = async (url, options = {}) => {
181
178
  calls.push({ url, options });
182
- if (url.endsWith('/domains/example.com')) {
183
- return { ok: true, status: 200, json: async () => ({}) };
179
+ if (url.endsWith('/domains?page_size=500')) {
180
+ return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
184
181
  }
185
- if (url.endsWith('/domains/example.com/records?page_size=500')) {
182
+ if (url.endsWith('/domains/42/records?page_size=500')) {
186
183
  return { ok: true, status: 200, json: async () => ({ data: [] }) };
187
184
  }
188
- if (url.endsWith('/domains/example.com/records') && options.method === 'POST') {
185
+ if (url.endsWith('/domains/42/records') && options.method === 'POST') {
189
186
  return { ok: true, status: 200, json: async () => ({ id: 222 }) };
190
187
  }
191
188
  return { ok: true, status: 204, json: async () => ({}) };
@@ -201,7 +198,7 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
201
198
  });
202
199
 
203
200
  await challenger.set(buildChallengeOpts());
204
- const postCall = calls.find((c) => c.url.endsWith('/domains/example.com/records') && c.options.method === 'POST');
201
+ const postCall = calls.find((c) => c.url.endsWith('/domains/42/records') && c.options.method === 'POST');
205
202
  assert.ok(postCall, 'expected TXT create call');
206
203
  const payload = JSON.parse(postCall.options.body);
207
204
  assert.strictEqual(payload.ttl_sec, 300);
@@ -212,10 +209,10 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
212
209
  const calls = [];
213
210
  global.fetch = async (url, options = {}) => {
214
211
  calls.push({ url, options });
215
- if (url.endsWith('/domains/example.com')) {
216
- return { ok: true, status: 200, json: async () => ({}) };
212
+ if (url.endsWith('/domains?page_size=500')) {
213
+ return { ok: true, status: 200, json: async () => ({ data: [{ id: 42, domain: 'example.com' }] }) };
217
214
  }
218
- if (url.endsWith('/domains/example.com/records?page_size=500')) {
215
+ if (url.endsWith('/domains/42/records?page_size=500')) {
219
216
  return { ok: true, status: 200, json: async () => ({ data: [{ id: 111, type: 'TXT', name: '_acme-challenge', target: 'test-token' }] }) };
220
217
  }
221
218
  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();
@@ -260,22 +261,40 @@ module.exports.create = function create(config = {}) {
260
261
  return response.json();
261
262
  }
262
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
+
263
279
  async function linodeUpsertTxtRecord(dnsHost, dnsAuthorization, altname) {
264
280
  const zoneCandidates = buildZoneCandidates({ dnsHost, altname });
265
281
  let lastError = null;
266
282
 
267
283
  for (const zone of zoneCandidates) {
284
+ let zoneInfo = null;
268
285
  try {
269
- await linodeRequest(`/domains/${encodeURIComponent(zone)}`, 'GET');
286
+ zoneInfo = await resolveLinodeZone(zone);
270
287
  } catch (error) {
271
288
  lastError = error;
272
289
  continue;
273
290
  }
291
+ if (!zoneInfo?.id) continue;
274
292
 
275
293
  const recordName = linodeRecordNameForHost(dnsHost, zone);
276
294
  if (!recordName && dnsHost !== zone) continue;
277
295
 
278
- const recordsResult = await linodeRequest(`/domains/${encodeURIComponent(zone)}/records?page_size=500`, 'GET');
296
+ const zoneId = zoneInfo.id;
297
+ const recordsResult = await linodeRequest(`/domains/${zoneId}/records?page_size=500`, 'GET');
279
298
  const existing = Array.isArray(recordsResult?.data) ? recordsResult.data : [];
280
299
  const sameRecord = existing.find((record) =>
281
300
  record?.type === 'TXT'
@@ -284,11 +303,11 @@ module.exports.create = function create(config = {}) {
284
303
  );
285
304
 
286
305
  if (sameRecord && sameRecord.id) {
287
- linodeTxtRecordsByHost.set(dnsHost, { zone, id: sameRecord.id });
288
- 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 };
289
308
  }
290
309
 
291
- const created = await linodeRequest(`/domains/${encodeURIComponent(zone)}/records`, 'POST', {
310
+ const created = await linodeRequest(`/domains/${zoneId}/records`, 'POST', {
292
311
  type: 'TXT',
293
312
  name: recordName,
294
313
  target: dnsAuthorization,
@@ -296,8 +315,8 @@ module.exports.create = function create(config = {}) {
296
315
  });
297
316
 
298
317
  if (created?.id) {
299
- linodeTxtRecordsByHost.set(dnsHost, { zone, id: created.id });
300
- 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 };
301
320
  }
302
321
  }
303
322
 
@@ -307,8 +326,8 @@ module.exports.create = function create(config = {}) {
307
326
 
308
327
  async function linodeRemoveTxtRecord(dnsHost) {
309
328
  const stored = linodeTxtRecordsByHost.get(dnsHost);
310
- if (!stored?.zone || !stored?.id) return false;
311
- await linodeRequest(`/domains/${encodeURIComponent(stored.zone)}/records/${stored.id}`, 'DELETE');
329
+ if (!stored?.zoneId || !stored?.id) return false;
330
+ await linodeRequest(`/domains/${stored.zoneId}/records/${stored.id}`, 'DELETE');
312
331
  linodeTxtRecordsByHost.delete(dnsHost);
313
332
  return true;
314
333
  }