From 618b87fc10365f581be829c026f2a2e8bb0e906a Mon Sep 17 00:00:00 2001 From: William Mantly Date: Fri, 10 Jul 2026 23:03:38 -0400 Subject: [PATCH 1/2] Fix Porkbun (and other zoneless providers) domain sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updateDomains passed `zoneId: domain.zoneId` unconditionally. Providers with no zone concept (Porkbun, DigitalOcean) return domains without a zoneId, so this sent an explicit `undefined`, which model-redis' processKeys rejects ("zoneId is not string type") and aborts the whole sync with a 422. Cloudflare was unaffected because its domains carry a real zoneId string. DnsProvider.create's catch only re-threw UnauthorizedDnsApi and swallowed everything else, returning undefined — so the route then crashed on `item.id` with an opaque "Cannot read properties of undefined" instead of the real validation error. - Omit zoneId from the Domain payload when the provider doesn't supply one. - Re-throw non-Unauthorized errors from create so failures surface properly. Co-Authored-By: Claude Opus 4.8 --- nodejs/models/dns_provider.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js index 105bdf8..7d1eefc 100644 --- a/nodejs/models/dns_provider.js +++ b/nodejs/models/dns_provider.js @@ -96,12 +96,15 @@ class DnsProvider extends Table{ }catch(error){ if(error.name === 'UnauthorizedDnsApi'){ let keys = []; - console.log('Provider', Provider) for(let key in Provider._keyMap){ keys.push({'key': key, message: 'Invalid Key'}) } throw this.errors.ObjectValidateError(keys, "API rejected key"); } + // Don't swallow other failures (e.g. a domain-sync validation error): + // returning undefined here made the route crash on `item.id` with an + // opaque message. Surface the real error to the caller instead. + throw error; } } @@ -145,7 +148,11 @@ class DnsProvider extends Table{ created_by: this.created_by, domain: domain.domain, dnsProvider_id: this.id, - zoneId: domain.zoneId, + // Only providers with a zone concept (e.g. Cloudflare) return a + // zoneId. Porkbun/DigitalOcean don't, and passing an explicit + // `undefined` trips model-redis' type check ("zoneId is not string + // type") and aborts the whole sync — so omit it when absent. + ...(domain.zoneId !== undefined ? {zoneId: domain.zoneId} : {}), }); } console.log('currentDomains:', currentDomains) From 39b4837fc69c66b937777f0ee862629185a31481 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Fri, 10 Jul 2026 23:17:43 -0400 Subject: [PATCH 2/2] Fix DigitalOcean domain sync (never set .domain) DigitalOcean.listDomains ran the API response through __parseRes, which does not map anything to `.domain` and additionally rewrites each item's `.name` to its subdomain via tld-extract. DO's /v2/domains returns objects keyed `name` with no zone id, so the domain objects had no usable `.domain` and Domain.create threw ObjectValidateError ("domain is not string type"), aborting the sync. Map name -> domain the way CloudFlare.listDomains does and skip __parseRes (it is for record responses). DO has no zone id; the earlier zoneId-omit fix covers that. Also request per_page=200 so accounts with >20 domains aren't truncated. Co-Authored-By: Claude Opus 4.8 --- nodejs/models/dns_provider/digitalocean.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nodejs/models/dns_provider/digitalocean.js b/nodejs/models/dns_provider/digitalocean.js index 264c827..fc3cad8 100644 --- a/nodejs/models/dns_provider/digitalocean.js +++ b/nodejs/models/dns_provider/digitalocean.js @@ -40,9 +40,17 @@ class DigitalOcean extends DnsApi{ } async listDomains(){ - let res = await this.axios('get', '/domains'); + // DO returns {domains:[{name, ttl, zone_file}]} keyed by `name` and has no + // zone id (API paths use the domain name directly). Map name -> domain the + // way CloudFlare.listDomains does; do NOT run this through __parseRes, which + // rewrites `.name` to its subdomain and would leave no usable domain name. + let res = await this.axios('get', '/domains?per_page=200'); - return this.__parseRes(res.data.domains); + for(let domain of res.data.domains){ + domain.domain = domain.name; + } + + return res.data.domains; } async getRecords(domain, options){