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.
204 lines
9.2 KiB
C#
204 lines
9.2 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
|
|
namespace Synor.Sdk.Hosting;
|
|
|
|
/// <summary>
|
|
/// Synor Hosting SDK client for C#.
|
|
/// Provides decentralized web hosting with domain management, DNS, deployments, and SSL.
|
|
/// </summary>
|
|
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<DomainAvailability> CheckAvailabilityAsync(string name, CancellationToken ct = default)
|
|
=> await GetAsync<DomainAvailability>($"/domains/check/{Uri.EscapeDataString(name)}", ct);
|
|
|
|
public async Task<Domain> RegisterDomainAsync(string name, RegisterDomainOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["name"] = name };
|
|
if (options?.Years != null) body["years"] = options.Years;
|
|
if (options?.AutoRenew != null) body["autoRenew"] = options.AutoRenew;
|
|
return await PostAsync<Domain>("/domains", body, ct);
|
|
}
|
|
|
|
public async Task<Domain> GetDomainAsync(string name, CancellationToken ct = default)
|
|
=> await GetAsync<Domain>($"/domains/{Uri.EscapeDataString(name)}", ct);
|
|
|
|
public async Task<List<Domain>> ListDomainsAsync(CancellationToken ct = default)
|
|
{
|
|
var response = await GetAsync<DomainsResponse>("/domains", ct);
|
|
return response.Domains ?? new List<Domain>();
|
|
}
|
|
|
|
public async Task<DomainRecord> ResolveDomainAsync(string name, CancellationToken ct = default)
|
|
=> await GetAsync<DomainRecord>($"/domains/{Uri.EscapeDataString(name)}/resolve", ct);
|
|
|
|
public async Task<Domain> RenewDomainAsync(string name, int years, CancellationToken ct = default)
|
|
=> await PostAsync<Domain>($"/domains/{Uri.EscapeDataString(name)}/renew", new { years }, ct);
|
|
|
|
// DNS Operations
|
|
public async Task<DnsZone> GetDnsZoneAsync(string domain, CancellationToken ct = default)
|
|
=> await GetAsync<DnsZone>($"/dns/{Uri.EscapeDataString(domain)}", ct);
|
|
|
|
public async Task<DnsZone> SetDnsRecordsAsync(string domain, IEnumerable<DnsRecord> records, CancellationToken ct = default)
|
|
=> await PutAsync<DnsZone>($"/dns/{Uri.EscapeDataString(domain)}", new { records }, ct);
|
|
|
|
public async Task<DnsZone> AddDnsRecordAsync(string domain, DnsRecord record, CancellationToken ct = default)
|
|
=> await PostAsync<DnsZone>($"/dns/{Uri.EscapeDataString(domain)}/records", record, ct);
|
|
|
|
// Deployment Operations
|
|
public async Task<Deployment> DeployAsync(string cid, DeployOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["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<Deployment>("/deployments", body, ct);
|
|
}
|
|
|
|
public async Task<Deployment> GetDeploymentAsync(string id, CancellationToken ct = default)
|
|
=> await GetAsync<Deployment>($"/deployments/{Uri.EscapeDataString(id)}", ct);
|
|
|
|
public async Task<List<Deployment>> ListDeploymentsAsync(string? domain = null, CancellationToken ct = default)
|
|
{
|
|
var path = domain != null ? $"/deployments?domain={Uri.EscapeDataString(domain)}" : "/deployments";
|
|
var response = await GetAsync<DeploymentsResponse>(path, ct);
|
|
return response.Deployments ?? new List<Deployment>();
|
|
}
|
|
|
|
public async Task<Deployment> RollbackAsync(string domain, string deploymentId, CancellationToken ct = default)
|
|
=> await PostAsync<Deployment>($"/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<Certificate> ProvisionSslAsync(string domain, ProvisionSslOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var body = options ?? new ProvisionSslOptions();
|
|
return await PostAsync<Certificate>($"/ssl/{Uri.EscapeDataString(domain)}", body, ct);
|
|
}
|
|
|
|
public async Task<Certificate> GetCertificateAsync(string domain, CancellationToken ct = default)
|
|
=> await GetAsync<Certificate>($"/ssl/{Uri.EscapeDataString(domain)}", ct);
|
|
|
|
public async Task<Certificate> RenewCertificateAsync(string domain, CancellationToken ct = default)
|
|
=> await PostAsync<Certificate>($"/ssl/{Uri.EscapeDataString(domain)}/renew", new { }, ct);
|
|
|
|
// Analytics
|
|
public async Task<AnalyticsData> GetAnalyticsAsync(string domain, AnalyticsOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var path = $"/sites/{Uri.EscapeDataString(domain)}/analytics";
|
|
var query = new List<string>();
|
|
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<AnalyticsData>(path, ct);
|
|
}
|
|
|
|
public async Task<long> PurgeCacheAsync(string domain, IEnumerable<string>? paths = null, CancellationToken ct = default)
|
|
{
|
|
var body = paths != null ? new { paths } : null;
|
|
var response = await DeleteAsync<PurgeResponse>($"/sites/{Uri.EscapeDataString(domain)}/cache", body, ct);
|
|
return response.Purged;
|
|
}
|
|
|
|
// Lifecycle
|
|
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
|
{
|
|
try { var r = await GetAsync<HostingHealthResponse>("/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<T> GetAsync<T>(string path, CancellationToken ct)
|
|
=> await ExecuteAsync(async () => {
|
|
var r = await _httpClient.GetAsync(path, ct);
|
|
await EnsureSuccessAsync(r);
|
|
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
|
});
|
|
|
|
private async Task<T> PostAsync<T>(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<T>(_jsonOptions, ct)!;
|
|
});
|
|
|
|
private async Task<T> PutAsync<T>(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<T>(_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<T> DeleteAsync<T>(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<T>(_jsonOptions, ct)!;
|
|
});
|
|
|
|
private async Task<T> ExecuteAsync<T>(Func<Task<T>> 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<Dictionary<string, object>>(c, _jsonOptions);
|
|
throw new HostingException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
|
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
|
}
|
|
}
|
|
}
|