using System.Net.Http.Json; using System.Reflection; using System.Runtime.InteropServices; namespace Zcbot.WindowsNode; internal static class EnrollmentClient { private static readonly string[] Capabilities = ["origin.plot@v2"]; internal static async Task EnrollAsync( EnrollOptions options, NodeConfigStore store, CancellationToken cancellationToken) { if (store.Exists) { throw new NodeConfigurationException( $"Node is already registered. Remove {store.ConfigPath} through the administrator recovery procedure before re-enrolling."); } var installId = Guid.NewGuid(); var request = new EnrollRequest( options.EnrollmentCode, options.NodeName, installId, Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.1.0", RuntimeInformation.OSDescription, Capabilities); using var client = new HttpClient { BaseAddress = options.ServerUrl, Timeout = TimeSpan.FromSeconds(30) }; using var response = await client.PostAsJsonAsync( "v1/software-nodes/enroll", request, cancellationToken); if (!response.IsSuccessStatusCode) { var detail = await response.Content.ReadAsStringAsync(cancellationToken); throw new NodeConfigurationException( $"Registration failed with HTTP {(int)response.StatusCode}: {Limit(detail, 300)}"); } var enrolled = await response.Content.ReadFromJsonAsync(cancellationToken) ?? throw new NodeConfigurationException("Registration response was empty."); if (enrolled.NodeId == Guid.Empty || string.IsNullOrWhiteSpace(enrolled.NodeToken)) { throw new NodeConfigurationException("Registration response did not contain a valid node identity."); } store.Save(new NodeConfig( options.ServerUrl, enrolled.NodeId, installId, options.NodeName, enrolled.NodeToken, Math.Clamp(enrolled.HeartbeatSeconds, 5, 300), Capabilities)); } private static string Limit(string value, int maxLength) => value.Length <= maxLength ? value : value[..maxLength]; }