import Foundation struct DomainAvailabilityService { private static let suggestionTLDs = ["net", "io", "dev", "app", "co", "org"] static func check(domain: String) async -> DomainAvailabilityResult { let normalizedDomain = normalize(domain) guard !normalizedDomain.isEmpty else { return DomainAvailabilityResult(domain: domain, status: .unknown) } if await checkViaRDAP(domain: normalizedDomain) == .registered { debugLog("rdap", domain: normalizedDomain, status: .registered) return DomainAvailabilityResult(domain: normalizedDomain, status: .registered) } let fallbackStatus = await checkViaDNSFallback(domain: normalizedDomain) let method = fallbackStatus == .registered ? "dns" : "fallback" debugLog(method, domain: normalizedDomain, status: fallbackStatus) return DomainAvailabilityResult(domain: normalizedDomain, status: fallbackStatus) } static func suggestions(for domain: String, limit: Int = 6) async -> [DomainSuggestionResult] { let normalizedDomain = normalize(domain) let candidates = suggestionCandidates(for: normalizedDomain, limit: limit) guard !candidates.isEmpty else { return [] } var results: [DomainSuggestionResult] = [] for candidate in candidates { if Task.isCancelled { break } let result = await check(domain: candidate) results.append(DomainSuggestionResult(domain: result.domain, status: result.status)) } return results } private static func checkViaRDAP(domain: String) async -> DomainAvailabilityStatus? { let status = await RDAPService.registrationStatus(for: domain) if status == nil { debugLog("rdap-not-found", domain: domain, details: "Ignoring unavailable response from rdap.org") } return status } private static func checkViaDNSFallback(domain: String) async -> DomainAvailabilityStatus { do { let aRecords = try await DNSLookupService.lookup(domain: domain, recordType: .A) if !aRecords.isEmpty { return .registered } } catch { debugLog("dns-a-error", domain: domain, details: error.localizedDescription) } do { let nsRecords = try await DNSLookupService.lookup(domain: domain, recordType: .NS) if !nsRecords.isEmpty { return .registered } return .unknown } catch { debugLog("dns-ns-error", domain: domain, details: error.localizedDescription) return .unknown } } private static func suggestionCandidates(for domain: String, limit: Int) -> [String] { let parts = domain.split(separator: ".") guard parts.count >= 2 else { return [] } let base = parts.dropLast().joined(separator: ".") let tld = String(parts.last ?? "") var candidates: [String] = [] for suggestionTLD in suggestionTLDs where suggestionTLD != tld { candidates.append("\(base).\(suggestionTLD)") if candidates.count == limit { return candidates } } if !base.contains("-"), base.count >= 6, candidates.count < limit { let midpoint = base.index(base.startIndex, offsetBy: base.count / 2) let hyphenated = "\(base[.. String { domain .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() } private static func debugLog(_ method: String, domain: String, status: DomainAvailabilityStatus) { #if DEBUG print("[Availability] \(domain) -> \(status.rawValue) via \(method)") #endif } private static func debugLog(_ method: String, domain: String, details: String) { #if DEBUG print("[Availability] \(domain) -> \(method): \(details)") #endif } }