|
|
@@ -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) {
|