Implements Database, Hosting, and Bridge SDKs for remaining languages: Swift SDKs: - SynorDatabase with KV, Document, Vector, TimeSeries stores - SynorHosting with domain, DNS, deployment, SSL operations - SynorBridge with lock-mint and burn-unlock cross-chain flows C SDKs: - database.h/c - multi-model database client - hosting.h/c - hosting and domain management - bridge.h/c - cross-chain asset transfers C++ SDKs: - database.hpp - modern C++17 with std::future async - hosting.hpp - domain and deployment operations - bridge.hpp - cross-chain bridge with wait operations C# SDKs: - SynorDatabase.cs - async/await with inner store classes - SynorHosting.cs - domain management and analytics - SynorBridge.cs - cross-chain with BridgeException handling Ruby SDKs: - synor_database - Struct-based types with Faraday HTTP - synor_hosting - domain, DNS, SSL, analytics - synor_bridge - lock-mint/burn-unlock with retry logic Phase 3 complete: Database/Hosting/Bridge now available in all 12 languages.
402 lines
14 KiB
Swift
402 lines
14 KiB
Swift
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<T: Decodable>(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<T: Decodable>(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<T: Decodable, R: Encodable>(path: String, body: R) async throws -> T {
|
|
return try await executeWithRetry {
|
|
try await self.performEncodableRequest(method: "POST", path: path, body: body)
|
|
}
|
|
}
|
|
|
|
private func put<T: Decodable>(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<T: Decodable, R: Encodable>(path: String, body: R) async throws -> T {
|
|
return try await executeWithRetry {
|
|
try await self.performEncodableRequest(method: "PUT", path: path, body: body)
|
|
}
|
|
}
|
|
|
|
private func patch<T: Decodable>(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<T: Decodable>(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<T: Decodable>(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<T: Decodable>(
|
|
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<T: Decodable, R: Encodable>(
|
|
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<T: Decodable>(_ 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<T>(_ operation: () async throws -> T) async throws -> T {
|
|
var lastError: Error?
|
|
|
|
for attempt in 0..<config.retries {
|
|
do {
|
|
return try await operation()
|
|
} catch {
|
|
lastError = error
|
|
if config.debug {
|
|
print("Attempt \(attempt + 1) failed: \(error)")
|
|
}
|
|
if attempt < config.retries - 1 {
|
|
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (1 << attempt)))
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError ?? HostingError.invalidResponse
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private struct EmptyHostingResponse: Decodable {}
|
|
|
|
private extension String {
|
|
var urlEncoded: String {
|
|
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
|
|
}
|
|
}
|