using System.Net.Http.Json; using System.Text.Json; namespace Synor.Sdk.Hosting; /// /// Synor Hosting SDK client for C#. /// Provides decentralized web hosting with domain management, DNS, deployments, and SSL. /// public class SynorHosting : IDisposable { private readonly HostingConfig _config; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions; private bool _disposed; public SynorHosting(HostingConfig config) { _config = config; _httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = config.Timeout }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0"); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true }; } // Domain Operations public async Task CheckAvailabilityAsync(string name, CancellationToken ct = default) => await GetAsync($"/domains/check/{Uri.EscapeDataString(name)}", ct); public async Task RegisterDomainAsync(string name, RegisterDomainOptions? options = null, CancellationToken ct = default) { var body = new Dictionary { ["name"] = name }; if (options?.Years != null) body["years"] = options.Years; if (options?.AutoRenew != null) body["autoRenew"] = options.AutoRenew; return await PostAsync("/domains", body, ct); } public async Task GetDomainAsync(string name, CancellationToken ct = default) => await GetAsync($"/domains/{Uri.EscapeDataString(name)}", ct); public async Task> ListDomainsAsync(CancellationToken ct = default) { var response = await GetAsync("/domains", ct); return response.Domains ?? new List(); } public async Task ResolveDomainAsync(string name, CancellationToken ct = default) => await GetAsync($"/domains/{Uri.EscapeDataString(name)}/resolve", ct); public async Task RenewDomainAsync(string name, int years, CancellationToken ct = default) => await PostAsync($"/domains/{Uri.EscapeDataString(name)}/renew", new { years }, ct); // DNS Operations public async Task GetDnsZoneAsync(string domain, CancellationToken ct = default) => await GetAsync($"/dns/{Uri.EscapeDataString(domain)}", ct); public async Task SetDnsRecordsAsync(string domain, IEnumerable records, CancellationToken ct = default) => await PutAsync($"/dns/{Uri.EscapeDataString(domain)}", new { records }, ct); public async Task AddDnsRecordAsync(string domain, DnsRecord record, CancellationToken ct = default) => await PostAsync($"/dns/{Uri.EscapeDataString(domain)}/records", record, ct); // Deployment Operations public async Task DeployAsync(string cid, DeployOptions? options = null, CancellationToken ct = default) { var body = new Dictionary { ["cid"] = cid }; if (options?.Domain != null) body["domain"] = options.Domain; if (options?.Subdomain != null) body["subdomain"] = options.Subdomain; if (options?.Spa != null) body["spa"] = options.Spa; if (options?.CleanUrls != null) body["cleanUrls"] = options.CleanUrls; return await PostAsync("/deployments", body, ct); } public async Task GetDeploymentAsync(string id, CancellationToken ct = default) => await GetAsync($"/deployments/{Uri.EscapeDataString(id)}", ct); public async Task> ListDeploymentsAsync(string? domain = null, CancellationToken ct = default) { var path = domain != null ? $"/deployments?domain={Uri.EscapeDataString(domain)}" : "/deployments"; var response = await GetAsync(path, ct); return response.Deployments ?? new List(); } public async Task RollbackAsync(string domain, string deploymentId, CancellationToken ct = default) => await PostAsync($"/deployments/{Uri.EscapeDataString(deploymentId)}/rollback", new { domain }, ct); public async Task DeleteDeploymentAsync(string id, CancellationToken ct = default) => await DeleteAsync($"/deployments/{Uri.EscapeDataString(id)}", ct); // SSL Operations public async Task ProvisionSslAsync(string domain, ProvisionSslOptions? options = null, CancellationToken ct = default) { var body = options ?? new ProvisionSslOptions(); return await PostAsync($"/ssl/{Uri.EscapeDataString(domain)}", body, ct); } public async Task GetCertificateAsync(string domain, CancellationToken ct = default) => await GetAsync($"/ssl/{Uri.EscapeDataString(domain)}", ct); public async Task RenewCertificateAsync(string domain, CancellationToken ct = default) => await PostAsync($"/ssl/{Uri.EscapeDataString(domain)}/renew", new { }, ct); // Analytics public async Task GetAnalyticsAsync(string domain, AnalyticsOptions? options = null, CancellationToken ct = default) { var path = $"/sites/{Uri.EscapeDataString(domain)}/analytics"; var query = new List(); if (options?.Period != null) query.Add($"period={Uri.EscapeDataString(options.Period)}"); if (options?.Start != null) query.Add($"start={Uri.EscapeDataString(options.Start)}"); if (options?.End != null) query.Add($"end={Uri.EscapeDataString(options.End)}"); if (query.Count > 0) path += "?" + string.Join("&", query); return await GetAsync(path, ct); } public async Task PurgeCacheAsync(string domain, IEnumerable? paths = null, CancellationToken ct = default) { var body = paths != null ? new { paths } : null; var response = await DeleteAsync($"/sites/{Uri.EscapeDataString(domain)}/cache", body, ct); return response.Purged; } // Lifecycle public async Task HealthCheckAsync(CancellationToken ct = default) { try { var r = await GetAsync("/health", ct); return r.Status == "healthy"; } catch { return false; } } public bool IsClosed => _disposed; public void Dispose() { if (!_disposed) { _httpClient.Dispose(); _disposed = true; } GC.SuppressFinalize(this); } // Private HTTP methods private async Task GetAsync(string path, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.GetAsync(path, ct); await EnsureSuccessAsync(r); return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; }); private async Task PostAsync(string path, object body, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); await EnsureSuccessAsync(r); return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; }); private async Task PutAsync(string path, object body, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.PutAsJsonAsync(path, body, _jsonOptions, ct); await EnsureSuccessAsync(r); return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; }); private async Task DeleteAsync(string path, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.DeleteAsync(path, ct); await EnsureSuccessAsync(r); return true; }); private async Task DeleteAsync(string path, object? body, CancellationToken ct) => await ExecuteAsync(async () => { var req = new HttpRequestMessage(HttpMethod.Delete, path); if (body != null) req.Content = JsonContent.Create(body, options: _jsonOptions); var r = await _httpClient.SendAsync(req, ct); await EnsureSuccessAsync(r); return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; }); private async Task ExecuteAsync(Func> op) { Exception? err = null; for (int i = 0; i < _config.Retries; i++) { try { return await op(); } catch (Exception ex) { err = ex; if (i < _config.Retries - 1) await Task.Delay(1000 << i); } } throw err!; } private async Task EnsureSuccessAsync(HttpResponseMessage r) { if (!r.IsSuccessStatusCode) { var c = await r.Content.ReadAsStringAsync(); var e = JsonSerializer.Deserialize>(c, _jsonOptions); throw new HostingException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); } } }