Files
ClawdDotNet/src/ClawdDotNet.Core/Deploymentcenter/TokenProvisioner.cs
T
RichardandClaude Opus 5.5 837a634a22 Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig
erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner
Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft.
Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch.

WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b
umbenannten Parameter angeglichen.

Dabei behoben:
- 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue"
  gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day.
- Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine
  CSS-Klasse gab - jetzt "success" wie laufende Instanzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:45:03 +02:00

131 lines
5.2 KiB
C#

using System.Text.Json;
namespace ClawdDotNet.Core.Deploymentcenter;
/// <summary>A sub-token issued for this instance.</summary>
/// <param name="Token">The plain text — delivered only once.</param>
/// <param name="TokenId">ID for revoking it in the administration.</param>
/// <param name="Scopes">Which rights were actually passed through.</param>
public sealed record ProvisionedToken(string Token, string TokenId, IReadOnlyList<string> Scopes);
/// <summary>
/// What a token says about itself (<c>GET /api/tokens/v1/whoami</c>, since SDK 2.7.1).
///
/// <para><see cref="ProjectSlug"/> 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 <b>zero findings</b> for a running, reporting instance. Supervision
/// would then look like "nothing unusual", and that is the worst of all
/// mistakes.</para>
/// </summary>
public sealed record TokenIdentity(
string TokenId,
string Name,
string Type,
string? ProjectSlug,
string Environment,
IReadOnlyList<string> Scopes)
{
/// <summary>Whether this token is bound to a project — see the class comment.</summary>
public bool IsProjectBound => !string.IsNullOrWhiteSpace(ProjectSlug);
}
/// <summary>
/// Access to <c>/api/tokens/v1/…</c>: issue a sub-token per instance
/// (<c>provision</c>) and ask a token about itself (<c>whoami</c>).
///
/// <para>This replaces the former self-registration via <c>POST /api/register</c> —
/// 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.</para>
///
/// <para>Rights can only be restricted in the process, never extended — what the
/// master token does not have, the sub-token does not get either.</para>
/// </summary>
public sealed class TokenProvisioner(DeploymentcenterApi api)
{
/// <summary>What an instance needs: send heartbeats and report errors.</summary>
public static readonly string[] InstanceScopes = ["watchdog:ping", "bugtracker:report"];
public async Task<ProvisionedToken> ProvisionAsync(
string clientName,
string instanceId,
IReadOnlyList<string> 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<string>();
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);
}
/// <summary>
/// Asks the token this connection works with about itself.
///
/// <para>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 <see cref="TokenIdentity"/> for why that is not unimportant.</para>
/// </summary>
public async Task<TokenIdentity> WhoAmIAsync(CancellationToken ct = default)
{
var response = await api.GetAsync("/api/tokens/v1/whoami", ct).ConfigureAwait(false);
var scopes = new List<string>();
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);
}
/// <summary>A string from the response; <c>null</c> also for JSON <c>null</c>.</summary>
private static string? Text(JsonElement root, string name)
=> root.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
}