using System.Text.Json; namespace ClawdDotNet.Core.Deploymentcenter; /// A sub-token issued for this instance. /// The plain text — delivered only once. /// ID for revoking it in the administration. /// Which rights were actually passed through. public sealed record ProvisionedToken(string Token, string TokenId, IReadOnlyList Scopes); /// /// What a token says about itself (GET /api/tokens/v1/whoami, since SDK 2.7.1). /// /// is the field that matters. Since SDK 2.8.1 /// a watchdog monitor inherits the project of its ping token — if nothing is entered there, /// the findings land in the phantom project "default", and a query for the own /// project returns zero findings for a running, reporting instance. Supervision /// would then look like "nothing unusual", and that is the worst of all /// mistakes. /// public sealed record TokenIdentity( string TokenId, string Name, string Type, string? ProjectSlug, string Environment, IReadOnlyList Scopes) { /// Whether this token is bound to a project — see the class comment. public bool IsProjectBound => !string.IsNullOrWhiteSpace(ProjectSlug); } /// /// Access to /api/tokens/v1/…: issue a sub-token per instance /// (provision) and ask a token about itself (whoami). /// /// This replaces the former self-registration via POST /api/register — /// that endpoint does not exist in the Deploymentcenter (and in the old WatchDog server it /// was the only way to get a token at all). The purpose stays the same /// and is worth keeping: afterwards the instances do not hold the /// master token, but a restricted one that can be revoked individually. /// /// Rights can only be restricted in the process, never extended — what the /// master token does not have, the sub-token does not get either. /// public sealed class TokenProvisioner(DeploymentcenterApi api) { /// What an instance needs: send heartbeats and report errors. public static readonly string[] InstanceScopes = ["watchdog:ping", "bugtracker:report"]; public async Task ProvisionAsync( string clientName, string instanceId, IReadOnlyList scopes, string environment = "production", CancellationToken ct = default) { var payload = new { client_name = clientName, instance_id = instanceId, scopes, environment }; var response = await api.PostAsync("/api/tokens/v1/provision", payload, ct) .ConfigureAwait(false); var token = response.TryGetProperty("sub_token", out var t) && t.ValueKind == JsonValueKind.String ? t.GetString() : null; if (string.IsNullOrWhiteSpace(token)) { throw new DeploymentcenterException( "no_token", "Die Provisionierung lieferte kein Token.", System.Net.HttpStatusCode.OK); } var tokenId = response.TryGetProperty("token_id", out var i) && i.ValueKind == JsonValueKind.String ? i.GetString() ?? "" : ""; var granted = new List(); if (response.TryGetProperty("scopes", out var s) && s.ValueKind == JsonValueKind.Array) { granted.AddRange(s.EnumerateArray() .Where(e => e.ValueKind == JsonValueKind.String) .Select(e => e.GetString()!)); } return new ProvisionedToken(token, tokenId, granted); } /// /// Asks the token this connection works with about itself. /// /// Not curiosity, but the only way to learn the project binding: /// it is not in the token and does not follow from what was sent along when /// issuing it — it is a property the server keeps. /// See for why that is not unimportant. /// public async Task WhoAmIAsync(CancellationToken ct = default) { var response = await api.GetAsync("/api/tokens/v1/whoami", ct).ConfigureAwait(false); var scopes = new List(); if (response.TryGetProperty("scopes", out var s) && s.ValueKind == JsonValueKind.Array) { scopes.AddRange(s.EnumerateArray() .Where(e => e.ValueKind == JsonValueKind.String) .Select(e => e.GetString()!)); } return new TokenIdentity( Text(response, "token_id") ?? "", Text(response, "name") ?? "", Text(response, "type") ?? "unknown", Text(response, "project_slug"), Text(response, "environment") ?? "", scopes); } /// A string from the response; null also for JSON null. private static string? Text(JsonElement root, string name) => root.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; }