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;
}