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>
131 lines
5.2 KiB
C#
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;
|
|
}
|