diff --git a/src/arvan-dns/arvan-dns.service.ts b/src/arvan-dns/arvan-dns.service.ts index d4858b5..4365217 100644 --- a/src/arvan-dns/arvan-dns.service.ts +++ b/src/arvan-dns/arvan-dns.service.ts @@ -18,7 +18,8 @@ type ArvanRecordType = 'aname' | 'cname' | 'a' | 'mx' | 'txt'; type DesiredRecord = { type: ArvanRecordType; name: string; - value: Record; + /** A records use an array of `{ ip }`; other types use a single object. */ + value: Record | Array>; }; type DnsMutationResult = { @@ -34,7 +35,7 @@ type ArvanDnsRecord = { name: string; ttl?: number; cloud?: boolean; - value?: Record | null; + value?: Record | Array> | null; }; @Injectable() @@ -110,7 +111,8 @@ export class ArvanDnsService { await this.upsertTyped(host, auth, existing, { type: 'a', name: 'mail', - value: { ip: MAIL_IPV4 }, + // Arvan CDN 4.0 requires A `value` as an array of IP objects. + value: [{ ip: MAIL_IPV4 }], }, created, updated, skipped); await this.replaceExtraMx(host, auth, existing, removed); @@ -447,7 +449,7 @@ export class ArvanDnsService { const mx = existing.filter((item) => item.type === 'mx'); const keep = mx.find((item) => { const name = this.relativeName(item.name, host); - return name === '@' && this.fqdn(this.mxHost(item.value ?? {})) === MAIL_HOSTNAME; + return name === '@' && this.fqdn(this.mxHost(this.recordValueObject(item.value))) === MAIL_HOSTNAME; }); for (const item of mx) { if (keep && item.id === keep.id) continue; @@ -462,7 +464,7 @@ export class ArvanDnsService { ); } const name = this.relativeName(item.name, host); - removed.push(`MX ${name} ${this.mxHost(item.value ?? {}) || item.id}`); + removed.push(`MX ${name} ${this.mxHost(this.recordValueObject(item.value)) || item.id}`); const index = existing.indexOf(item); if (index >= 0) existing.splice(index, 1); } @@ -498,27 +500,51 @@ export class ArvanDnsService { } private sameTarget(desired: DesiredRecord, existing: ArvanDnsRecord): boolean { - const current = existing.value ?? {}; + const current = this.recordValueObject(existing.value); + const desiredObj = this.recordValueObject(desired.value); if (desired.type === 'aname') { - return this.fqdn(String(current.location ?? '')) === this.fqdn(String(desired.value.location ?? '')); + return this.fqdn(String(current.location ?? '')) === this.fqdn(String(desiredObj.location ?? '')); } if (desired.type === 'a') { - return String(current.ip ?? '') === String(desired.value.ip ?? ''); + return this.aIps(existing.value).join(',') === this.aIps(desired.value).join(','); } if (desired.type === 'mx') { return ( - this.fqdn(this.mxHost(current)) === this.fqdn(String(desired.value.host ?? '')) && - Number(current.priority ?? 0) === Number(desired.value.priority ?? 0) + this.fqdn(this.mxHost(current)) === this.fqdn(String(desiredObj.host ?? '')) && + Number(current.priority ?? 0) === Number(desiredObj.priority ?? 0) ); } if (desired.type === 'txt') { - return this.txtValue(existing).replace(/\s+/g, '') === String(desired.value.text ?? '').replace(/\s+/g, ''); + return this.txtValue(existing).replace(/\s+/g, '') === String(desiredObj.text ?? '').replace(/\s+/g, ''); } - return this.fqdn(String(current.host ?? '')) === this.fqdn(String(desired.value.host ?? '')); + return this.fqdn(String(current.host ?? '')) === this.fqdn(String(desiredObj.host ?? '')); + } + + /** Arvan A records return `value` as `[{ ip }]`; normalize to IP list for compare/send. */ + private aIps(value: DesiredRecord['value'] | ArvanDnsRecord['value']): string[] { + if (Array.isArray(value)) { + return value + .map((item) => String(item?.ip ?? '').trim()) + .filter(Boolean); + } + if (value && typeof value === 'object' && 'ip' in value) { + const ip = String(value.ip ?? '').trim(); + return ip ? [ip] : []; + } + return []; + } + + private recordValueObject( + value: DesiredRecord['value'] | ArvanDnsRecord['value'], + ): Record { + if (Array.isArray(value)) { + return (value[0] as Record | undefined) ?? {}; + } + return value ?? {}; } private txtValue(record: ArvanDnsRecord): string { - const value = record.value ?? {}; + const value = this.recordValueObject(record.value); if (typeof value.text === 'string') return value.text; if (value.txt && typeof value.txt === 'object' && 'text' in value.txt) { return String((value.txt as { text?: unknown }).text ?? '');