using static StellaOps.Localization.T; namespace StellaOps.Audit.ReplayToken; /// /// A deterministic, content-addressable replay token with optional expiration. /// public sealed partial class ReplayToken : IEquatable { public const string Scheme = "replay"; public const string DefaultAlgorithm = "SHA-256"; public const string DefaultVersion = "1.0"; /// /// Version 2.0 includes expiration support. /// public const string VersionWithExpiration = "2.0"; /// /// Default expiration duration (1 hour). /// public static readonly TimeSpan DefaultExpiration = TimeSpan.FromHours(1); /// /// The token value (SHA-256 hash in hex). /// public string Value { get; } /// /// Algorithm used for hashing. /// public string Algorithm { get; } /// /// Version of the token generation algorithm. /// public string Version { get; } /// /// Timestamp when token was generated. /// public DateTimeOffset GeneratedAt { get; } /// /// Timestamp when token expires. Null means no expiration (v1.0 behavior). /// public DateTimeOffset? ExpiresAt { get; } /// /// Canonical representation for storage. /// For v2.0+, includes expiration timestamp. /// public string Canonical => ExpiresAt.HasValue ? $"{Scheme}:v{Version}:{Algorithm}:{Value}:{ExpiresAt.Value.ToUnixTimeSeconds()}" : $"{Scheme}:v{Version}:{Algorithm}:{Value}"; /// /// Creates a replay token without expiration (v1.0 compatibility). /// public ReplayToken(string value, DateTimeOffset generatedAt, string? algorithm = null, string? version = null) : this(value, generatedAt, null, algorithm, version) { } /// /// Creates a replay token with optional expiration. /// public ReplayToken(string value, DateTimeOffset generatedAt, DateTimeOffset? expiresAt, string? algorithm = null, string? version = null) { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException(_t("common.audit.replay_token_value_empty"), nameof(value)); } Value = value.Trim(); GeneratedAt = generatedAt; ExpiresAt = expiresAt; Algorithm = string.IsNullOrWhiteSpace(algorithm) ? DefaultAlgorithm : algorithm.Trim(); if (expiresAt.HasValue && string.IsNullOrWhiteSpace(version)) { Version = VersionWithExpiration; } else { Version = string.IsNullOrWhiteSpace(version) ? DefaultVersion : version.Trim(); } } }