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);
}
}
}