import Foundation /// Synor Hosting SDK client for Swift. /// /// Provides decentralized web hosting with domain management, DNS, deployments, SSL, and analytics. /// /// Example: /// ```swift /// let hosting = SynorHosting(config: HostingConfig(apiKey: "your-api-key")) /// /// // Check domain availability /// let availability = try await hosting.checkAvailability(name: "mydomain.synor") /// if availability.available { /// // Register domain /// let domain = try await hosting.registerDomain(name: "mydomain.synor") /// print("Registered: \(domain.name)") /// } /// /// // Deploy content /// let deployment = try await hosting.deploy(cid: "Qm...", options: DeployOptions(domain: "mydomain.synor")) /// print("Deployed to: \(deployment.url)") /// /// // Provision SSL /// let cert = try await hosting.provisionSsl(domain: "mydomain.synor") /// print("SSL status: \(cert.status)") /// ``` public class SynorHosting { private let config: HostingConfig private let session: URLSession private let decoder: JSONDecoder private let encoder: JSONEncoder private var closed = false public init(config: HostingConfig) { self.config = config let sessionConfig = URLSessionConfiguration.default sessionConfig.timeoutIntervalForRequest = config.timeout sessionConfig.timeoutIntervalForResource = config.timeout * 2 self.session = URLSession(configuration: sessionConfig) self.decoder = JSONDecoder() self.encoder = JSONEncoder() } // MARK: - Domain Operations /// Check domain availability. public func checkAvailability(name: String) async throws -> DomainAvailability { return try await get(path: "/domains/check/\(name.urlEncoded)") } /// Register a new domain. public func registerDomain(name: String, options: RegisterDomainOptions? = nil) async throws -> Domain { var body: [String: Any] = ["name": name] if let options = options { if let years = options.years { body["years"] = years } if let autoRenew = options.autoRenew { body["autoRenew"] = autoRenew } } return try await post(path: "/domains", body: body) } /// Get domain details. public func getDomain(name: String) async throws -> Domain { return try await get(path: "/domains/\(name.urlEncoded)") } /// List all domains. public func listDomains() async throws -> [Domain] { let response: DomainsResponse = try await get(path: "/domains") return response.domains ?? [] } /// Update domain record. public func updateDomainRecord(name: String, record: DomainRecord) async throws -> Domain { return try await put(path: "/domains/\(name.urlEncoded)/record", body: record) } /// Resolve domain to its record. public func resolveDomain(name: String) async throws -> DomainRecord { return try await get(path: "/domains/\(name.urlEncoded)/resolve") } /// Renew a domain. public func renewDomain(name: String, years: Int) async throws -> Domain { return try await post(path: "/domains/\(name.urlEncoded)/renew", body: ["years": years]) } // MARK: - DNS Operations /// Get DNS zone for a domain. public func getDnsZone(domain: String) async throws -> DnsZone { return try await get(path: "/dns/\(domain.urlEncoded)") } /// Set DNS records for a domain. public func setDnsRecords(domain: String, records: [DnsRecord]) async throws -> DnsZone { return try await put(path: "/dns/\(domain.urlEncoded)", body: ["records": records]) } /// Add a DNS record. public func addDnsRecord(domain: String, record: DnsRecord) async throws -> DnsZone { return try await post(path: "/dns/\(domain.urlEncoded)/records", body: record) } /// Delete a DNS record. public func deleteDnsRecord(domain: String, recordType: String, name: String) async throws -> DnsZone { return try await delete(path: "/dns/\(domain.urlEncoded)/records/\(recordType)/\(name.urlEncoded)") } // MARK: - Deployment Operations /// Deploy content to a domain. public func deploy(cid: String, options: DeployOptions? = nil) async throws -> Deployment { var body: [String: Any] = ["cid": cid] if let options = options { if let domain = options.domain { body["domain"] = domain } if let subdomain = options.subdomain { body["subdomain"] = subdomain } if let spa = options.spa { body["spa"] = spa } if let cleanUrls = options.cleanUrls { body["cleanUrls"] = cleanUrls } if let trailingSlash = options.trailingSlash { body["trailingSlash"] = trailingSlash } } return try await post(path: "/deployments", body: body) } /// Get deployment details. public func getDeployment(id: String) async throws -> Deployment { return try await get(path: "/deployments/\(id.urlEncoded)") } /// List deployments. public func listDeployments(domain: String? = nil) async throws -> [Deployment] { var path = "/deployments" if let domain = domain { path += "?domain=\(domain.urlEncoded)" } let response: DeploymentsResponse = try await get(path: path) return response.deployments ?? [] } /// Rollback to a previous deployment. public func rollback(domain: String, deploymentId: String) async throws -> Deployment { return try await post( path: "/deployments/\(deploymentId.urlEncoded)/rollback", body: ["domain": domain] ) } /// Delete a deployment. public func deleteDeployment(id: String) async throws { let _: EmptyHostingResponse = try await deleteRequest(path: "/deployments/\(id.urlEncoded)") } // MARK: - SSL Operations /// Provision SSL certificate for a domain. public func provisionSsl(domain: String, options: ProvisionSslOptions? = nil) async throws -> Certificate { var body: [String: Any] = [:] if let options = options { if let includeWww = options.includeWww { body["includeWww"] = includeWww } if let autoRenew = options.autoRenew { body["autoRenew"] = autoRenew } } return try await post(path: "/ssl/\(domain.urlEncoded)", body: body.isEmpty ? nil : body) } /// Get certificate details. public func getCertificate(domain: String) async throws -> Certificate { return try await get(path: "/ssl/\(domain.urlEncoded)") } /// Renew SSL certificate. public func renewCertificate(domain: String) async throws -> Certificate { return try await post(path: "/ssl/\(domain.urlEncoded)/renew", body: nil as [String: Any]?) } /// Delete SSL certificate. public func deleteCertificate(domain: String) async throws { let _: EmptyHostingResponse = try await deleteRequest(path: "/ssl/\(domain.urlEncoded)") } // MARK: - Site Configuration /// Get site configuration. public func getSiteConfig(domain: String) async throws -> SiteConfig { return try await get(path: "/sites/\(domain.urlEncoded)/config") } /// Update site configuration. public func updateSiteConfig(domain: String, config: [String: Any]) async throws -> SiteConfig { return try await patch(path: "/sites/\(domain.urlEncoded)/config", body: config) } /// Purge cache. public func purgeCache(domain: String, paths: [String]? = nil) async throws -> Int64 { let body: [String: Any]? = paths.map { ["paths": $0] } let response: PurgeResponse = try await deleteRequest( path: "/sites/\(domain.urlEncoded)/cache", body: body ) return response.purged } // MARK: - Analytics /// Get analytics data for a domain. public func getAnalytics(domain: String, options: AnalyticsOptions? = nil) async throws -> AnalyticsData { var params: [String] = [] if let period = options?.period { params.append("period=\(period.urlEncoded)") } if let start = options?.start { params.append("start=\(start.urlEncoded)") } if let end = options?.end { params.append("end=\(end.urlEncoded)") } var path = "/sites/\(domain.urlEncoded)/analytics" if !params.isEmpty { path += "?\(params.joined(separator: "&"))" } return try await get(path: path) } // MARK: - Lifecycle /// Close the client. public func close() { closed = true session.invalidateAndCancel() } /// Check if the client is closed. public var isClosed: Bool { closed } /// Perform a health check. public func healthCheck() async -> Bool { do { let response: HostingHealthResponse = try await get(path: "/health") return response.status == "healthy" } catch { return false } } // MARK: - Private HTTP Methods private func get(path: String) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "GET", path: path, body: nil as [String: Any]?) } } private func post(path: String, body: [String: Any]?) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "POST", path: path, body: body) } } private func post(path: String, body: R) async throws -> T { return try await executeWithRetry { try await self.performEncodableRequest(method: "POST", path: path, body: body) } } private func put(path: String, body: [String: Any]) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "PUT", path: path, body: body) } } private func put(path: String, body: R) async throws -> T { return try await executeWithRetry { try await self.performEncodableRequest(method: "PUT", path: path, body: body) } } private func patch(path: String, body: [String: Any]) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "PATCH", path: path, body: body) } } private func delete(path: String) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "DELETE", path: path, body: nil as [String: Any]?) } } private func deleteRequest(path: String, body: [String: Any]? = nil) async throws -> T { return try await executeWithRetry { try await self.performRequest(method: "DELETE", path: path, body: body) } } private func performRequest( method: String, path: String, body: [String: Any]? ) async throws -> T { if closed { throw HostingError.clientClosed } guard let url = URL(string: "\(config.endpoint)\(path)") else { throw HostingError.invalidResponse } var request = URLRequest(url: url) request.httpMethod = method request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version") if let body = body { do { request.httpBody = try JSONSerialization.data(withJSONObject: body) } catch { throw HostingError.encodingError(error) } } return try await executeRequest(request) } private func performEncodableRequest( method: String, path: String, body: R ) async throws -> T { if closed { throw HostingError.clientClosed } guard let url = URL(string: "\(config.endpoint)\(path)") else { throw HostingError.invalidResponse } var request = URLRequest(url: url) request.httpMethod = method request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version") do { request.httpBody = try encoder.encode(body) } catch { throw HostingError.encodingError(error) } return try await executeRequest(request) } private func executeRequest(_ request: URLRequest) async throws -> T { let (data, response): (Data, URLResponse) do { (data, response) = try await session.data(for: request) } catch { throw HostingError.networkError(error) } guard let httpResponse = response as? HTTPURLResponse else { throw HostingError.invalidResponse } if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { if data.isEmpty && T.self == EmptyHostingResponse.self { return EmptyHostingResponse() as! T } do { return try decoder.decode(T.self, from: data) } catch { throw HostingError.decodingError(error) } } let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any] let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)" let code = errorInfo?["code"] as? String throw HostingError.httpError(statusCode: httpResponse.statusCode, message: message, code: code) } private func executeWithRetry(_ operation: () async throws -> T) async throws -> T { var lastError: Error? for attempt in 0..