Fix DuckDNS double-suffixing a subdomain that already includes .duckdns.org (#128)

Reported error when adding a DuckDNS provider with
subdomains="nl-theta42.duckdns.org" (the full name, as DuckDNS's own
site displays it):

  {"name": "EntryNotFound", "message": "Domain:duckdns.org does not exists"}

listDomains() blindly appended ".duckdns.org" to whatever was entered,
turning "nl-theta42.duckdns.org" into
"nl-theta42.duckdns.org.duckdns.org". tld-extract doesn't know
duckdns.org is a shared suffix, so it parsed that malformed string
down to domain "duckdns.org" — surfacing as a confusing EntryNotFound
two layers away from the actual cause (Domain.create's internal
lookup).

Add __normalizeLabel() to strip a trailing ".duckdns.org" (and
lowercase) before use, so both "myhost" and "myhost.duckdns.org" work
identically. Also make __label()'s existing suffix-strip
case-insensitive to match.
This commit is contained in:
2026-07-14 12:29:06 -04:00
committed by GitHub
parent 1c7ad9aaae
commit e091c15d95
2 changed files with 33 additions and 2 deletions
+14 -2
View File
@@ -54,6 +54,18 @@ class DuckDns extends DnsApi{
if(status !== 'OK') throw this.errors.unauthorized();
}
// Accepts either the bare label ("myhost") or the full duckdns.org name
// ("myhost.duckdns.org", how DuckDNS's own site displays it, and what
// operators naturally paste in) — strip a trailing ".duckdns.org" so
// both forms end up as the same label. Without this, "myhost.duckdns.org"
// would get double-suffixed to "myhost.duckdns.org.duckdns.org", which
// tld-extract (not aware duckdns.org is a shared suffix) then misparses
// as domain "duckdns.org" — surfacing as a confusing "Domain:duckdns.org
// does not exists" error two layers away from the actual cause.
__normalizeLabel(value){
return value.replace(/\.duckdns\.org$/i, '').toLowerCase();
}
// No API to enumerate owned subdomains, so the operator supplies them.
// This call both validates the token and, as a side effect, syncs each
// domain's A/AAAA record to this host's current public IP if the token
@@ -61,14 +73,14 @@ class DuckDns extends DnsApi{
// — the same thing an operator would need to do anyway when pointing a
// fresh DuckDNS domain at this proxy.
async listDomains(){
let labels = this.subdomains.split(',').map(d => d.trim()).filter(Boolean);
let labels = this.subdomains.split(',').map(d => this.__normalizeLabel(d.trim())).filter(Boolean);
await this.update(labels.join(','), {});
return labels.map(label => ({domain: `${label}.duckdns.org`}));
}
__label(domain){
return domain.domain.replace(/\.duckdns\.org$/, '');
return domain.domain.replace(/\.duckdns\.org$/i, '');
}
// No read API exists; public DNS is the only source of truth available.
@@ -200,6 +200,25 @@ describe('DNS Provider Contract Compliance', () => {
const instance = new DuckDns({token: 'mock-token', subdomains: 'mockhost'});
assert.strictEqual(instance.__label({domain: 'mockhost.duckdns.org'}), 'mockhost');
});
test('__normalizeLabel accepts both the bare label and the full duckdns.org name', () => {
const instance = new DuckDns({token: 'mock-token', subdomains: 'mockhost'});
assert.strictEqual(instance.__normalizeLabel('mockhost'), 'mockhost');
assert.strictEqual(instance.__normalizeLabel('mockhost.duckdns.org'), 'mockhost');
assert.strictEqual(instance.__normalizeLabel('MockHost.DuckDNS.org'), 'mockhost');
});
test('listDomains does not double-suffix a subdomains value that already includes .duckdns.org', async () => {
const instance = new DuckDns({token: 'mock-token', subdomains: 'nl-theta42.duckdns.org,other'});
// Stub out the network call — this test is only about what domain
// name(s) listDomains() builds from `subdomains`, not the live API.
instance.update = async () => {};
const domains = await instance.listDomains();
assert.deepStrictEqual(domains, [
{domain: 'nl-theta42.duckdns.org'},
{domain: 'other.duckdns.org'},
]);
});
});
});