import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js'; import type { IEmailDnsRecordIntent, IEmailDomainOperationError, TEmailDnsVerificationOutcome, } from '../../ts_interfaces/data/email-domain.js'; import { classifyMailTxtRecord, type IMailDnsTxtPolicyResult, } from './mail-dns-txt-policy.js'; export interface IMailDnsMutationClient { createRecord(args: { domainId: string; name: string; type: 'MX' | 'TXT' | 'A' | 'AAAA'; value: string; ttl: number; createdBy: string; managedBy: string; managedOwnerId: string; managedRecordKey: string; }): Promise<{ success: boolean; id?: string; message?: string }>; updateRecord(args: { id: string; name?: string; type?: 'MX' | 'TXT' | 'A' | 'AAAA'; value?: string; ttl?: number; }): Promise<{ success: boolean; message?: string }>; deleteRecord(id: string): Promise<{ success: boolean; message?: string }>; } export interface IReconcileMailDnsSingletonOptions { intent: IEmailDnsRecordIntent; rrset: DnsRecordDoc[]; dnsManager: IMailDnsMutationClient; managedBy: string; createdBy: string; evaluate: ( intent: IEmailDnsRecordIntent, values: string[], ) => Promise; } const outcomeError = ( intent: IEmailDnsRecordIntent, outcome: Exclude, message: string, ): IEmailDomainOperationError => { let code: string; if (outcome === 'missing') code = 'DNS_RECORD_MISSING'; else if (outcome === 'propagation') code = 'DNS_PROPAGATION_PENDING'; else if (outcome === 'duplicate') code = 'DNS_PUBLIC_SINGLETON_DUPLICATE'; else if (outcome === 'conflict') code = 'DNS_SINGLETON_CONFLICT'; else if (outcome === 'invalid') code = 'DNS_SINGLETON_INVALID'; else { code = intent.value.trim().toLowerCase().startsWith('v=spf1') ? 'DNS_SPF_AUTHORIZATION_LOOKUP_FAILED' : 'DNS_LOOKUP_FAILED'; } return { code, message, recordKey: intent.key, retryable: outcome === 'missing' || outcome === 'propagation' || outcome === 'lookup-error', }; }; export function applyMailDnsVerificationResult( intent: IEmailDnsRecordIntent, result: IMailDnsTxtPolicyResult, phase: 'reconcile' | 'verify', ): void { intent.verificationOutcome = result.outcome; if (result.outcome === 'valid') { intent.error = undefined; intent.effectiveValue = result.effectiveValue; intent.status = phase === 'verify' ? 'valid' : 'satisfied'; if (phase === 'verify') intent.validatedAt = new Date().toISOString(); return; } intent.status = result.outcome === 'conflict' || result.outcome === 'duplicate' ? 'conflict' : result.outcome === 'invalid' ? 'failed' : 'pending'; intent.error = outcomeError( intent, result.outcome, result.reason || `${intent.type} ${intent.name} did not validate`, ); } function isSingletonAuthenticationIntent(intent: IEmailDnsRecordIntent): boolean { if (intent.type !== 'TXT') return false; const normalized = intent.value.trim().toLowerCase(); return normalized.startsWith('v=spf1') || normalized.startsWith('v=dmarc1') || normalized.startsWith('v=dkim1'); } function exactIntentOwner( record: DnsRecordDoc, intent: IEmailDnsRecordIntent, managedBy: string, ): boolean { return record.managedBy === managedBy && record.managedOwnerId === intent.ownerId && record.managedRecordKey === intent.key; } async function deleteOwnedRecords( records: DnsRecordDoc[], options: IReconcileMailDnsSingletonOptions, ): Promise { for (const record of records) { const result = await options.dnsManager.deleteRecord(record.id); if (!result.success) { options.intent.status = 'pending'; options.intent.verificationOutcome = 'duplicate'; options.intent.error = { code: 'DNS_SINGLETON_DUPLICATE_CLEANUP_FAILED', message: result.message || `Failed to remove duplicate ${options.intent.type} ${options.intent.name}`, recordKey: options.intent.key, retryable: true, }; return false; } } return true; } /** * Reconcile SPF, DMARC, and DKIM singleton TXT records without adopting or * mutating operator policy. Returns false for non-singleton intents. */ export async function reconcileMailDnsSingleton( options: IReconcileMailDnsSingletonOptions, ): Promise { const { intent, rrset, managedBy } = options; if (!isSingletonAuthenticationIntent(intent)) return false; const exactOwned = rrset .filter((record) => exactIntentOwner(record, intent, managedBy)) .sort((left, right) => left.id.localeCompare(right.id)); const otherManaged = rrset.filter((record) => ( record.managedBy === managedBy && !exactIntentOwner(record, intent, managedBy) )); const operator = rrset.filter((record) => record.managedBy !== managedBy); const relevantOtherManaged = otherManaged.filter((record) => ( classifyMailTxtRecord(intent, record.value).relevant )); const relevantOperator = operator.filter((record) => ( classifyMailTxtRecord(intent, record.value).relevant )); if (relevantOperator.length > 0) { if (!await deleteOwnedRecords(exactOwned, options)) return true; if (relevantOtherManaged.length > 0) { applyMailDnsVerificationResult(intent, { outcome: 'conflict', reason: `Another managed owner controls ${intent.type} ${intent.name}`, }, 'reconcile'); return true; } const result = await options.evaluate(intent, relevantOperator.map((record) => record.value)); applyMailDnsVerificationResult(intent, result, 'reconcile'); if (result.outcome === 'valid') intent.satisfactionSource = 'operator'; return true; } if (relevantOtherManaged.length > 0) { if (!await deleteOwnedRecords(exactOwned, options)) return true; applyMailDnsVerificationResult(intent, { outcome: 'conflict', reason: `Another managed owner controls ${intent.type} ${intent.name}`, }, 'reconcile'); return true; } const managed = exactOwned[0]; if (exactOwned.length > 1 && !await deleteOwnedRecords(exactOwned.slice(1), options)) { return true; } if (managed) { if ( managed.value.trim() !== intent.value.trim() || managed.name !== intent.name || managed.type !== intent.type || managed.ttl !== intent.ttl ) { const result = await options.dnsManager.updateRecord({ id: managed.id, name: intent.name, type: intent.type, value: intent.value, ttl: intent.ttl, }); if (!result.success) throw new Error(result.message || `Failed to update ${intent.key}`); } intent.providerRecordId = managed.providerRecordId; intent.status = 'satisfied'; intent.satisfactionSource = 'managed'; intent.effectiveValue = intent.value; intent.verificationOutcome = undefined; intent.error = undefined; intent.lastAttemptAt = new Date().toISOString(); return true; } const result = await options.dnsManager.createRecord({ domainId: intent.domainId, name: intent.name, type: intent.type, value: intent.value, ttl: intent.ttl, createdBy: options.createdBy, managedBy, managedOwnerId: intent.ownerId, managedRecordKey: intent.key, }); if (!result.success) throw new Error(result.message || `Failed to create ${intent.key}`); intent.status = 'satisfied'; intent.satisfactionSource = 'managed'; intent.effectiveValue = intent.value; intent.verificationOutcome = undefined; intent.error = undefined; intent.lastAttemptAt = new Date().toISOString(); return true; }