Răsfoiți Sursa

fix: discover cloudflare parent zones for subdomains

AI-Co-Authored-By: Codex
chendeben 1 lună în urmă
părinte
comite
a745e96daa
2 a modificat fișierele cu 78 adăugiri și 8 ștergeri
  1. 41 8
      src/dns-providers.js
  2. 37 0
      test/dns-providers.test.js

+ 41 - 8
src/dns-providers.js

@@ -52,6 +52,7 @@ class CloudflareProvider {
     this.credentials = credential.credentials || {};
     this.zoneName = credential.zoneName;
     this.ttl = credential.defaultTtl || 600;
+    this.zoneIdCache = new Map();
   }
 
   async test() {
@@ -102,25 +103,43 @@ class CloudflareProvider {
   }
 
   async zoneId(record, domain) {
-    const targetZoneName = this.zoneNameFor(record, domain);
+    const targetZoneName = await this.resolveZoneName(record, domain);
     if (this.credentials.zoneId && (!targetZoneName || sameZone(targetZoneName, this.zoneName))) {
       return this.credentials.zoneId;
     }
     if (!targetZoneName) throw new Error('Cloudflare 需要 zoneName、zoneId 或发信域名。');
-    const response = await this.request(`/zones?name=${encodeURIComponent(targetZoneName)}`);
-    const zone = response.result?.[0];
-    if (!zone?.id) throw new Error(`Cloudflare 未找到 Zone ${targetZoneName}。`);
-    return zone.id;
+    return this.lookupZoneId(targetZoneName);
   }
 
-  zoneNameFor(record, domain) {
+  async resolveZoneName(record, domain) {
     const host = record?.host || '';
     const domainName = domain?.domain || '';
     if (this.zoneName && (!host || isHostInZone(host, this.zoneName))) return this.zoneName;
-    if (domainName && (!host || isHostInZone(host, domainName))) return domainName;
+    const candidates = cloudflareZoneCandidates(domainName || host);
+    for (const candidate of candidates) {
+      const zoneId = await this.lookupZoneId(candidate, { optional: true });
+      if (zoneId) return candidate;
+    }
     return this.zoneName || domainName;
   }
 
+  async lookupZoneId(zoneName, { optional = false } = {}) {
+    const cleanZone = normalizeZoneName(zoneName);
+    if (!cleanZone) return '';
+    if (this.zoneIdCache.has(cleanZone)) return this.zoneIdCache.get(cleanZone);
+    const response = await this.request(`/zones?name=${encodeURIComponent(cleanZone)}`);
+    const zone = response.result?.[0];
+    if (!zone?.id) {
+      if (optional) {
+        this.zoneIdCache.set(cleanZone, '');
+        return '';
+      }
+      throw new Error(`Cloudflare 未找到 Zone ${cleanZone}。`);
+    }
+    this.zoneIdCache.set(cleanZone, zone.id);
+    return zone.id;
+  }
+
   async request(path, options = {}) {
     if (!this.credentials.apiToken) throw new Error('Cloudflare API Token 不能为空。');
     const response = await fetch(`${CLOUDFLARE_API}${path}`, {
@@ -344,7 +363,21 @@ function effectiveZoneName(credential, domain, record) {
 }
 
 function sameZone(left, right) {
-  return String(left || '').replace(/\.$/, '').toLowerCase() === String(right || '').replace(/\.$/, '').toLowerCase();
+  return normalizeZoneName(left) === normalizeZoneName(right);
+}
+
+function normalizeZoneName(value) {
+  return String(value || '').replace(/\.$/, '').toLowerCase();
+}
+
+function cloudflareZoneCandidates(name) {
+  const clean = normalizeZoneName(name);
+  const parts = clean.split('.').filter(Boolean);
+  const candidates = [];
+  for (let index = 0; index <= parts.length - 2; index += 1) {
+    candidates.push(parts.slice(index).join('.'));
+  }
+  return candidates;
 }
 
 function outOfZoneResult(record, zoneName) {

+ 37 - 0
test/dns-providers.test.js

@@ -153,6 +153,43 @@ test('cloudflare one-click dns can use the current domain zone with a multi-zone
   assert.ok(calls.some((call) => call.method === 'POST' && call.url.includes('/zones/zone-other/dns_records')));
 });
 
+test('cloudflare one-click dns discovers the parent zone for subdomain sending domains', async () => {
+  const calls = [];
+  globalThis.fetch = async (url, options = {}) => {
+    calls.push({ url: String(url), method: options.method || 'GET' });
+    if (String(url).includes('/zones?name=sender.a4sky.com')) {
+      return json({ success: true, result: [] });
+    }
+    if (String(url).includes('/zones?name=a4sky.com')) {
+      return json({ success: true, result: [{ id: 'zone-a4sky', name: 'a4sky.com' }] });
+    }
+    if (String(url).includes('/dns_records?')) return json({ success: true, result: [] });
+    return json({ success: true, result: { id: 'ok' } });
+  };
+
+  const result = await applyDnsSetup(
+    { ...domainFixture(), domain: 'sender.a4sky.com', senderHost: 'in.ss5.xyz' },
+    cloudflareCredential(),
+    {
+      records: [
+        {
+          key: 'verification',
+          host: '_mailhub.sender.a4sky.com',
+          type: 'TXT',
+          value: 'mailhub-verification=token',
+          status: 'missing'
+        }
+      ]
+    }
+  );
+
+  assert.equal(result.ok, true);
+  assert.equal(result.results[0].ok, true);
+  assert.ok(calls.some((call) => call.url.includes('/zones?name=sender.a4sky.com')));
+  assert.ok(calls.some((call) => call.url.includes('/zones?name=a4sky.com')));
+  assert.ok(calls.some((call) => call.method === 'POST' && call.url.includes('/zones/zone-a4sky/dns_records')));
+});
+
 function cloudflareCredential() {
   return {
     provider: 'cloudflare',