using System.Text.Json; namespace ClawdDotNet.Core.Deploymentcenter; /// Ein für diese Instanz ausgestelltes Sub-Token. /// Der Klartext — wird nur einmal ausgeliefert. /// Kennung zum Widerrufen in der Verwaltung. /// Welche Rechte tatsächlich durchgereicht wurden. public sealed record ProvisionedToken(string Token, string TokenId, IReadOnlyList Scopes); /// /// Was ein Token über sich selbst sagt (GET /api/tokens/v1/whoami, seit SDK 2.7.1). /// /// ist das Feld, auf das es ankommt. Seit SDK 2.8.1 erbt /// ein Watchdog-Monitor das Projekt seines Ping-Tokens — ist dort nichts eingetragen, /// landen die Befunde im Scheinprojekt „default", und eine Abfrage nach dem eigenen /// Projekt liefert für eine laufende, meldende Instanz null Befunde. Die Aufsicht /// sähe dann aus wie „nichts auffällig", und das ist der schlechteste aller /// Irrtümer. /// public sealed record TokenIdentity( string TokenId, string Name, string Type, string? ProjectSlug, string Environment, IReadOnlyList Scopes) { /// Ob dieses Token an ein Projekt gebunden ist — siehe Klassenkommentar. public bool IsProjectBound => !string.IsNullOrWhiteSpace(ProjectSlug); } /// /// Der Zugang zu /api/tokens/v1/…: ein Sub-Token je Instanz ausstellen /// (provision) und ein Token nach sich selbst fragen (whoami). /// /// Das ersetzt die frühere Selbstregistrierung über POST /api/register — /// diesen Endpunkt gibt es im Deploymentcenter nicht (und im alten WatchDog-Server war /// er der einzige Weg, überhaupt an einen Token zu kommen). Der Zweck bleibt derselbe /// und ist es wert, erhalten zu bleiben: Auf den Instanzen liegt danach nicht das /// Master-Token, sondern ein eingeschränktes, einzeln widerrufbares. /// /// Rechte lassen sich dabei nur einschränken, nie erweitern — was das /// Master-Token nicht hat, bekommt auch das Sub-Token nicht. /// public sealed class TokenProvisioner(DeploymentcenterApi api) { /// Was eine Instanz braucht: Heartbeats senden und Fehler melden. 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); } /// /// Fragt das Token, mit dem diese Verbindung arbeitet, nach sich selbst. /// /// Nicht Neugier, sondern die einzige Möglichkeit, die Projektbindung zu /// erfahren: Sie steht nicht im Token und ergibt sich nicht aus dem, was beim /// Ausstellen mitgeschickt wurde — sie ist eine Eigenschaft, die der Server führt. /// Siehe , warum das nicht gleichgültig ist. /// 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); } /// Eine Zeichenkette aus der Antwort; null auch bei JSON-null. private static string? Text(JsonElement root, string name) => root.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; }