// GoDaddyService.swift // CxIDE — GoDaddy API integration for domain and DNS management // // Supports both OTE (test) and Production environments. // API docs: https://developer.godaddy.com/doc import Foundation // MARK: - GoDaddy Service final class GoDaddyService: @unchecked Sendable { enum Environment: String, CaseIterable, Sendable { case ote = "OTE (Test)" case production = "Production" var baseURL: String { switch self { case .ote: return "https://api.ote-godaddy.com" case .production: return "https://api.godaddy.com" } } } private let credentialStore: CredentialStore var environment: Environment init(credentialStore: CredentialStore = .shared, environment: Environment = .ote) { self.credentialStore = credentialStore self.environment = environment } // MARK: - Auth Header private var authHeader: String? { let key: String? let secret: String? switch environment { case .ote: key = credentialStore.godaddyOTEKey secret = credentialStore.godaddyOTESecret case .production: key = credentialStore.godaddyProdKey secret = credentialStore.godaddyProdSecret } guard let k = key, let s = secret, !k.isEmpty, !s.isEmpty else { return nil } return "sso-key \(k):\(s)" } // MARK: - Domain Operations /// List all domains in the account func listDomains() async throws -> [GoDaddyDomain] { let data = try await request(path: "/v1/domains", method: "GET") return try JSONDecoder().decode([GoDaddyDomain].self, from: data) } /// Get domain details func getDomain(_ domain: String) async throws -> GoDaddyDomainDetail { let data = try await request(path: "/v1/domains/\(domain)", method: "GET") return try JSONDecoder().decode(GoDaddyDomainDetail.self, from: data) } /// Check domain availability func checkAvailability(domain: String) async throws -> GoDaddyAvailability { let data = try await request(path: "/v1/domains/available?domain=\(domain)", method: "GET") return try JSONDecoder().decode(GoDaddyAvailability.self, from: data) } /// Get suggested domains func suggestDomains(query: String, limit: Int = 10) async throws -> [GoDaddySuggestion] { let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query let data = try await request( path: "/v1/domains/suggest?query=\(encoded)&limit=\(limit)", method: "GET" ) return try JSONDecoder().decode([GoDaddySuggestion].self, from: data) } // MARK: - DNS Records /// Get all DNS records for a domain func getDNSRecords(domain: String) async throws -> [DNSRecord] { let data = try await request(path: "/v1/domains/\(domain)/records", method: "GET") return try JSONDecoder().decode([DNSRecord].self, from: data) } /// Get DNS records by type func getDNSRecords(domain: String, type: DNSRecordType) async throws -> [DNSRecord] { let data = try await request( path: "/v1/domains/\(domain)/records/\(type.rawValue)", method: "GET" ) return try JSONDecoder().decode([DNSRecord].self, from: data) } /// Add DNS records func addDNSRecords(domain: String, records: [DNSRecord]) async throws { let body = try JSONEncoder().encode(records) _ = try await request(path: "/v1/domains/\(domain)/records", method: "PATCH", body: body) } /// Replace all DNS records of a specific type and name func replaceDNSRecord(domain: String, type: DNSRecordType, name: String, records: [DNSRecord]) async throws { let body = try JSONEncoder().encode(records) _ = try await request( path: "/v1/domains/\(domain)/records/\(type.rawValue)/\(name)", method: "PUT", body: body ) } /// Delete all DNS records of a specific type and name func deleteDNSRecord(domain: String, type: DNSRecordType, name: String) async throws { _ = try await request( path: "/v1/domains/\(domain)/records/\(type.rawValue)/\(name)", method: "DELETE" ) } // MARK: - Convenience: Point domain to hosting /// Point a domain's A record to an IP address (for hosting) func pointDomainToIP(domain: String, ip: String, ttl: Int = 600) async throws { let record = DNSRecord(type: .A, name: "@", data: ip, ttl: ttl) try await replaceDNSRecord(domain: domain, type: .A, name: "@", records: [record]) } /// Set a CNAME record (e.g., www -> hosting provider) func setCNAME(domain: String, name: String, target: String, ttl: Int = 3600) async throws { let record = DNSRecord(type: .CNAME, name: name, data: target, ttl: ttl) try await replaceDNSRecord(domain: domain, type: .CNAME, name: name, records: [record]) } /// Configure DNS for Netlify hosting func configureForNetlify(domain: String, netlifySubdomain: String) async throws { // Point @ to Netlify's load balancer try await pointDomainToIP(domain: domain, ip: "75.2.60.5") // Point www to Netlify subdomain try await setCNAME(domain: domain, name: "www", target: "\(netlifySubdomain).netlify.app") } /// Configure DNS for GitHub Pages func configureForGitHubPages(domain: String, githubUsername: String) async throws { // GitHub Pages IPs let githubIPs = ["185.199.108.153", "185.199.109.153", "185.199.110.153", "185.199.111.153"] let records = githubIPs.map { DNSRecord(type: .A, name: "@", data: $0, ttl: 600) } try await replaceDNSRecord(domain: domain, type: .A, name: "@", records: records) try await setCNAME(domain: domain, name: "www", target: "\(githubUsername).github.io") } /// Configure DNS for Vercel hosting func configureForVercel(domain: String) async throws { try await pointDomainToIP(domain: domain, ip: "76.76.21.21") try await setCNAME(domain: domain, name: "www", target: "cname.vercel-dns.com") } /// Configure DNS for Cloudflare Pages func configureForCloudflare(domain: String, projectName: String) async throws { try await setCNAME(domain: domain, name: "@", target: "\(projectName).pages.dev") try await setCNAME(domain: domain, name: "www", target: "\(projectName).pages.dev") } // MARK: - HTTP Client private func request(path: String, method: String, body: Data? = nil) async throws -> Data { guard let auth = authHeader else { throw GoDaddyError.notAuthenticated } let urlString = environment.baseURL + path guard let url = URL(string: urlString) else { throw GoDaddyError.invalidURL(urlString) } var req = URLRequest(url: url) req.httpMethod = method req.setValue(auth, forHTTPHeaderField: "Authorization") req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("application/json", forHTTPHeaderField: "Accept") req.timeoutInterval = 30 if let body = body { req.httpBody = body } let (data, response) = try await URLSession.shared.data(for: req) guard let httpResponse = response as? HTTPURLResponse else { throw GoDaddyError.invalidResponse } switch httpResponse.statusCode { case 200...299: return data case 401: throw GoDaddyError.unauthorized case 403: throw GoDaddyError.forbidden case 404: throw GoDaddyError.notFound(path) case 422: let msg = String(data: data, encoding: .utf8) ?? "Validation error" throw GoDaddyError.validationError(msg) case 429: throw GoDaddyError.rateLimited default: let msg = String(data: data, encoding: .utf8) ?? "HTTP \(httpResponse.statusCode)" throw GoDaddyError.apiError(httpResponse.statusCode, msg) } } } // MARK: - Models struct GoDaddyDomain: Codable, Sendable { let domain: String let status: String? let expires: String? let expirationProtected: Bool? let holdRegistrar: Bool? let locked: Bool? let privacy: Bool? let renewAuto: Bool? let renewable: Bool? let transferProtected: Bool? } struct GoDaddyDomainDetail: Codable, Sendable { let domain: String let domainId: Int? let status: String? let expires: String? let nameServers: [String]? let locked: Bool? let privacy: Bool? let renewAuto: Bool? let contactAdmin: GoDaddyContact? let contactRegistrant: GoDaddyContact? } struct GoDaddyContact: Codable, Sendable { let email: String? let nameFirst: String? let nameLast: String? let organization: String? let phone: String? } struct GoDaddyAvailability: Codable, Sendable { let available: Bool let domain: String let definitive: Bool? let price: Int? let currency: String? let period: Int? } struct GoDaddySuggestion: Codable, Sendable { let domain: String } enum DNSRecordType: String, Codable, Sendable, CaseIterable { case A, AAAA, CNAME, MX, NS, SOA, SRV, TXT, CAA } struct DNSRecord: Codable, Sendable { let type: DNSRecordType let name: String let data: String var ttl: Int? var priority: Int? var port: Int? var weight: Int? var service: String? var `protocol`: String? init(type: DNSRecordType, name: String, data: String, ttl: Int? = nil, priority: Int? = nil) { self.type = type self.name = name self.data = data self.ttl = ttl self.priority = priority } } // MARK: - Errors enum GoDaddyError: LocalizedError { case notAuthenticated case unauthorized case forbidden case notFound(String) case invalidURL(String) case invalidResponse case validationError(String) case rateLimited case apiError(Int, String) var errorDescription: String? { switch self { case .notAuthenticated: return "GoDaddy API credentials not configured" case .unauthorized: return "Invalid GoDaddy API credentials" case .forbidden: return "Access forbidden — check API key permissions" case .notFound(let path): return "Resource not found: \(path)" case .invalidURL(let url): return "Invalid URL: \(url)" case .invalidResponse: return "Invalid response from GoDaddy API" case .validationError(let msg): return "Validation error: \(msg)" case .rateLimited: return "GoDaddy API rate limit exceeded" case .apiError(let code, let msg): return "GoDaddy API error (\(code)): \(msg)" } } }