# frozen_string_literal: true require "faraday" require "json" require "uri" module SynorHosting # Synor Hosting SDK client for Ruby. # Provides domain management, DNS, deployments, SSL, and analytics. class Client attr_reader :closed def initialize(config) @config = config @closed = false @conn = Faraday.new(url: config.endpoint) do |f| f.request :json f.response :json f.options.timeout = config.timeout f.headers["Authorization"] = "Bearer #{config.api_key}" f.headers["Content-Type"] = "application/json" f.headers["X-SDK-Version"] = "ruby/#{VERSION}" end end # ==================== Domain Operations ==================== # Register a new domain def register_domain(name, auto_renew: true) body = { name: name, auto_renew: auto_renew } response = post("/domains", body) parse_domain(response) end # Get domain details def get_domain(name) response = get("/domains/#{encode(name)}") parse_domain(response) end # Resolve domain to its record def resolve_domain(name) response = get("/domains/#{encode(name)}/resolve") DomainRecord.new( cid: response["cid"], ttl: response["ttl"], updated_at: response["updated_at"] ) end # Update domain record def update_domain(name, cid:, ttl: nil) body = { cid: cid } body[:ttl] = ttl if ttl response = put("/domains/#{encode(name)}", body) parse_domain(response) end # Transfer domain ownership def transfer_domain(name, new_owner) body = { new_owner: new_owner } response = post("/domains/#{encode(name)}/transfer", body) parse_domain(response) end # List domains def list_domains(limit: nil, offset: nil) params = {} params[:limit] = limit if limit params[:offset] = offset if offset response = get("/domains", params) (response["domains"] || []).map { |d| parse_domain(d) } end # ==================== DNS Operations ==================== # Set DNS records for a domain def set_dns_records(domain, records) body = { records: records.map { |r| record_to_hash(r) } } response = put("/domains/#{encode(domain)}/dns", body) (response["records"] || []).map { |r| parse_dns_record(r) } end # Get DNS records for a domain def get_dns_records(domain) response = get("/domains/#{encode(domain)}/dns") (response["records"] || []).map { |r| parse_dns_record(r) } end # Add a single DNS record def add_dns_record(domain, record) body = record_to_hash(record) response = post("/domains/#{encode(domain)}/dns", body) parse_dns_record(response) end # Delete a DNS record def delete_dns_record(domain, record_type, name) delete("/domains/#{encode(domain)}/dns/#{record_type}/#{encode(name)}") nil end # ==================== Deployment Operations ==================== # Deploy content to a domain def deploy(cid, domain, options = {}) body = { cid: cid, domain: domain } body[:build_command] = options[:build_command] if options[:build_command] body[:env_vars] = options[:env_vars] if options[:env_vars] response = post("/deployments", body) parse_deployment(response) end # Get deployment details def get_deployment(id) response = get("/deployments/#{encode(id)}") parse_deployment(response) end # List deployments def list_deployments(domain: nil, limit: nil, offset: nil) params = {} params[:domain] = domain if domain params[:limit] = limit if limit params[:offset] = offset if offset response = get("/deployments", params) (response["deployments"] || []).map { |d| parse_deployment(d) } end # Wait for deployment to complete def wait_for_deployment(id, poll_interval: 5, max_wait: 600) deadline = Time.now + max_wait final_statuses = [DeploymentStatus::ACTIVE, DeploymentStatus::FAILED, DeploymentStatus::ROLLED_BACK] while Time.now < deadline deployment = get_deployment(id) return deployment if final_statuses.include?(deployment.status) sleep(poll_interval) end raise Error, "Timeout waiting for deployment" end # Rollback deployment def rollback(domain, deployment_id) body = { deployment_id: deployment_id } response = post("/domains/#{encode(domain)}/rollback", body) parse_deployment(response) end # ==================== SSL Operations ==================== # Provision SSL certificate for a domain def provision_ssl(domain) response = post("/domains/#{encode(domain)}/ssl", {}) parse_certificate(response) end # Get SSL certificate for a domain def get_certificate(domain) response = get("/domains/#{encode(domain)}/ssl") parse_certificate(response) end # Renew SSL certificate def renew_certificate(domain) response = post("/domains/#{encode(domain)}/ssl/renew", {}) parse_certificate(response) end # ==================== Analytics Operations ==================== # Get analytics for a domain def get_analytics(domain, start_time:, end_time:) params = { start: start_time.to_i, end: end_time.to_i } response = get("/domains/#{encode(domain)}/analytics", params) parse_analytics(response) end # Get bandwidth stats def get_bandwidth(domain, start_time:, end_time:) params = { start: start_time.to_i, end: end_time.to_i } response = get("/domains/#{encode(domain)}/analytics/bandwidth", params) BandwidthStats.new( total_bytes: response["total_bytes"], cached_bytes: response["cached_bytes"], uncached_bytes: response["uncached_bytes"] ) end # ==================== Lifecycle ==================== # Health check def health_check response = get("/health") response["status"] == "healthy" rescue StandardError false end # Close the client def close @closed = true @conn.close if @conn.respond_to?(:close) end private def get(path, params = {}) execute { @conn.get(path, params).body } end def post(path, body) execute { @conn.post(path, body).body } end def put(path, body) execute { @conn.put(path, body).body } end def delete(path) execute { @conn.delete(path).body } end def execute raise ClientClosedError, "Client has been closed" if @closed last_error = nil @config.retries.times do |attempt| begin response = yield check_error(response) if response.is_a?(Hash) return response rescue StandardError => e last_error = e sleep(2**attempt) if attempt < @config.retries - 1 end end raise last_error end def check_error(response) return unless response["error"] || response["code"] message = response["message"] || response["error"] || "Unknown error" code = response["code"] status = response["status_code"] || 0 raise HttpError.new(message, status_code: status, code: code) end def encode(str) URI.encode_www_form_component(str) end def record_to_hash(record) hash = { type: record.type, name: record.name, value: record.value, ttl: record.ttl } hash[:priority] = record.priority if record.priority hash end def parse_domain(data) return nil unless data Domain.new( name: data["name"], owner: data["owner"], status: data["status"], record: data["record"] ? DomainRecord.new( cid: data["record"]["cid"], ttl: data["record"]["ttl"], updated_at: data["record"]["updated_at"] ) : nil, expires_at: data["expires_at"], created_at: data["created_at"], auto_renew: data["auto_renew"] ) end def parse_dns_record(data) return nil unless data DnsRecord.new( type: data["type"], name: data["name"], value: data["value"], ttl: data["ttl"], priority: data["priority"] ) end def parse_deployment(data) return nil unless data Deployment.new( id: data["id"], domain: data["domain"], cid: data["cid"], status: data["status"], url: data["url"], created_at: data["created_at"], deployed_at: data["deployed_at"], build_logs: data["build_logs"], error_message: data["error_message"] ) end def parse_certificate(data) return nil unless data Certificate.new( domain: data["domain"], status: data["status"], issuer: data["issuer"], issued_at: data["issued_at"], expires_at: data["expires_at"], auto_renew: data["auto_renew"], fingerprint: data["fingerprint"] ) end def parse_analytics(data) return nil unless data Analytics.new( domain: data["domain"], time_range: AnalyticsTimeRange.new( start_time: data["time_range"]&.dig("start"), end_time: data["time_range"]&.dig("end") ), requests: data["requests"] ? RequestStats.new( total: data["requests"]["total"], success: data["requests"]["success"], error: data["requests"]["error"], cached: data["requests"]["cached"] ) : nil, bandwidth: data["bandwidth"] ? BandwidthStats.new( total_bytes: data["bandwidth"]["total_bytes"], cached_bytes: data["bandwidth"]["cached_bytes"], uncached_bytes: data["bandwidth"]["uncached_bytes"] ) : nil, visitors: data["visitors"], page_views: data["page_views"], top_paths: (data["top_paths"] || []).map do |p| PathStats.new(path: p["path"], requests: p["requests"], bandwidth: p["bandwidth"]) end, geo_distribution: (data["geo_distribution"] || []).map do |g| GeoStats.new(country: g["country"], requests: g["requests"], bandwidth: g["bandwidth"]) end ) end end end