sprints work
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// A deterministic, content-addressable replay token.
|
||||
/// A deterministic, content-addressable replay token with optional expiration.
|
||||
/// </summary>
|
||||
public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
{
|
||||
@@ -9,6 +9,16 @@ public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
public const string DefaultAlgorithm = "SHA-256";
|
||||
public const string DefaultVersion = "1.0";
|
||||
|
||||
/// <summary>
|
||||
/// Version 2.0 includes expiration support.
|
||||
/// </summary>
|
||||
public const string VersionWithExpiration = "2.0";
|
||||
|
||||
/// <summary>
|
||||
/// Default expiration duration (1 hour).
|
||||
/// </summary>
|
||||
public static readonly TimeSpan DefaultExpiration = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// The token value (SHA-256 hash in hex).
|
||||
/// </summary>
|
||||
@@ -30,11 +40,30 @@ public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
public DateTimeOffset GeneratedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Canonical representation for storage.
|
||||
/// Timestamp when token expires. Null means no expiration (v1.0 behavior).
|
||||
/// </summary>
|
||||
public string Canonical => $"{Scheme}:v{Version}:{Algorithm}:{Value}";
|
||||
public DateTimeOffset? ExpiresAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Canonical representation for storage.
|
||||
/// For v2.0+, includes expiration timestamp.
|
||||
/// </summary>
|
||||
public string Canonical => ExpiresAt.HasValue
|
||||
? $"{Scheme}:v{Version}:{Algorithm}:{Value}:{ExpiresAt.Value.ToUnixTimeSeconds()}"
|
||||
: $"{Scheme}:v{Version}:{Algorithm}:{Value}";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a replay token without expiration (v1.0 compatibility).
|
||||
/// </summary>
|
||||
public ReplayToken(string value, DateTimeOffset generatedAt, string? algorithm = null, string? version = null)
|
||||
: this(value, generatedAt, null, algorithm, version)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a replay token with optional expiration.
|
||||
/// </summary>
|
||||
public ReplayToken(string value, DateTimeOffset generatedAt, DateTimeOffset? expiresAt, string? algorithm = null, string? version = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
@@ -43,12 +72,56 @@ public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
|
||||
Value = value.Trim();
|
||||
GeneratedAt = generatedAt;
|
||||
ExpiresAt = expiresAt;
|
||||
Algorithm = string.IsNullOrWhiteSpace(algorithm) ? DefaultAlgorithm : algorithm.Trim();
|
||||
Version = string.IsNullOrWhiteSpace(version) ? DefaultVersion : version.Trim();
|
||||
|
||||
// Default to v2.0 if expiration is set, otherwise use provided or default
|
||||
if (expiresAt.HasValue && string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
Version = VersionWithExpiration;
|
||||
}
|
||||
else
|
||||
{
|
||||
Version = string.IsNullOrWhiteSpace(version) ? DefaultVersion : version.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the token has expired.
|
||||
/// </summary>
|
||||
/// <param name="currentTime">The current time to check against. If null, uses DateTimeOffset.UtcNow.</param>
|
||||
/// <returns>True if the token has expired; false if not expired or has no expiration.</returns>
|
||||
public bool IsExpired(DateTimeOffset? currentTime = null)
|
||||
{
|
||||
if (!ExpiresAt.HasValue)
|
||||
{
|
||||
return false; // v1.0 tokens without expiration never expire
|
||||
}
|
||||
|
||||
var now = currentTime ?? DateTimeOffset.UtcNow;
|
||||
return now >= ExpiresAt.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining time until expiration.
|
||||
/// </summary>
|
||||
/// <param name="currentTime">The current time to check against. If null, uses DateTimeOffset.UtcNow.</param>
|
||||
/// <returns>The remaining time, or null if no expiration or already expired.</returns>
|
||||
public TimeSpan? GetTimeToExpiration(DateTimeOffset? currentTime = null)
|
||||
{
|
||||
if (!ExpiresAt.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = currentTime ?? DateTimeOffset.UtcNow;
|
||||
var remaining = ExpiresAt.Value - now;
|
||||
return remaining > TimeSpan.Zero ? remaining : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a canonical token string.
|
||||
/// Supports both v1.0 format (4 parts) and v2.0 format with expiration (5 parts).
|
||||
/// </summary>
|
||||
public static ReplayToken Parse(string canonical)
|
||||
{
|
||||
@@ -58,7 +131,10 @@ public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
}
|
||||
|
||||
var parts = canonical.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 4 || !string.Equals(parts[0], Scheme, StringComparison.Ordinal))
|
||||
|
||||
// v1.0 format: replay:v1.0:SHA-256:hash (4 parts)
|
||||
// v2.0 format: replay:v2.0:SHA-256:hash:expiry_unix_seconds (5 parts)
|
||||
if (parts.Length < 4 || parts.Length > 5 || !string.Equals(parts[0], Scheme, StringComparison.Ordinal))
|
||||
{
|
||||
throw new FormatException($"Invalid replay token format: {canonical}");
|
||||
}
|
||||
@@ -73,7 +149,35 @@ public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
var algorithm = parts[2];
|
||||
var value = parts[3];
|
||||
|
||||
return new ReplayToken(value, DateTimeOffset.UnixEpoch, algorithm, version);
|
||||
DateTimeOffset? expiresAt = null;
|
||||
if (parts.Length == 5)
|
||||
{
|
||||
if (!long.TryParse(parts[4], out var unixSeconds))
|
||||
{
|
||||
throw new FormatException($"Invalid expiration timestamp in replay token: {canonical}");
|
||||
}
|
||||
expiresAt = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
|
||||
}
|
||||
|
||||
return new ReplayToken(value, DateTimeOffset.UnixEpoch, expiresAt, algorithm, version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to parse a canonical token string.
|
||||
/// </summary>
|
||||
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
||||
public static bool TryParse(string canonical, out ReplayToken? token)
|
||||
{
|
||||
try
|
||||
{
|
||||
token = Parse(canonical);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Canonical;
|
||||
|
||||
Reference in New Issue
Block a user