using System.Diagnostics.CodeAnalysis;
namespace DodoSSH.Client.Domain;
///
/// A Spelunk API key and the one Spelunk it may be sent to, decrypted.
///
///
///
/// The address is what makes this an item of its own. A key kept as a credential's password can be
/// sent anywhere a host names, and a link from Spelunk can be sent by anything on the machine — so a link
/// that is not already backed by a saved host has to stop and ask. A key stored with its address says, once
/// and in the keychain, where the user agreed it may go: a link from that Spelunk connects straight away, and
/// a host naming this key with any other address is refused at connect rather than sending it there. See
/// VaultViewModel.OpenSpelunkLinkAsync.
///
///
/// is a password, and everything this codebase does about passwords applies to
/// it, as it does to : it is inside the encrypted payload,
/// the activity log records that it changed and never what it was, and the merge reports that it differed
/// rather than either value.
///
///
public sealed record SpelunkKeySecret : IVaultSecret
{
/// The longest API key stored.
///
/// Far beyond any real key, for the reason exists: an
/// unbounded string is the shape a corrupted payload takes, and this one goes into a request header.
///
public const int MaxApiKeyLength = 4096;
/// What the user calls this key.
public required string Label { get; init; }
///
/// The Spelunk API this key is for, as writes it, and the
/// only address it is ever sent to.
///
///
/// Normalised for the reason is: it is compared, byte for byte, with
/// the address a host or a link names.
///
public required string ApiBase { get; init; }
/// The API key itself, as Spelunk issued it.
public required string ApiKey { get; init; }
/// Whether this is storable, and why not if it is not.
public bool TryValidate([NotNullWhen(false)] out string? reason)
{
if (string.IsNullOrWhiteSpace(Label))
{
reason = "A Spelunk key needs a name.";
return false;
}
if (string.IsNullOrWhiteSpace(ApiBase))
{
reason = "A Spelunk key needs the address of the Spelunk it is for.";
return false;
}
if (!SpelunkRoute.TryNormalizeApiBase(ApiBase, out var normalized, out reason))
{
return false;
}
if (!string.Equals(normalized, ApiBase, StringComparison.Ordinal))
{
reason = "The Spelunk API address is not in its normal form.";
return false;
}
if (string.IsNullOrWhiteSpace(ApiKey))
{
reason = "A Spelunk key needs the key.";
return false;
}
// A header value cannot hold a line break, and a key with one would fail at the first connect with an
// HTTP error that names neither the key nor this item.
if (ApiKey.Length > MaxApiKeyLength || ApiKey.Any(char.IsControl))
{
reason = "That does not look like a Spelunk API key.";
return false;
}
reason = null;
return true;
}
}