using System.Text.Json; using System.Text.Json.Serialization; using static StellaOps.Localization.T; namespace StellaOps.Audit.ReplayToken; public sealed partial class Sha256ReplayTokenGenerator { private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private static string? NormalizeValue(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return value.Trim(); } private static List NormalizeSortedList(IReadOnlyList? values) { if (values is null || values.Count == 0) { return new List(); } return values .Where(static x => !string.IsNullOrWhiteSpace(x)) .Select(static x => x.Trim()) .OrderBy(static x => x, StringComparer.Ordinal) .ToList(); } private static Dictionary NormalizeSortedDictionary(IReadOnlyDictionary? values) { if (values is null || values.Count == 0) { return new Dictionary(); } var normalized = new List>(values.Count); var seen = new HashSet(StringComparer.Ordinal); foreach (var kvp in values) { if (string.IsNullOrWhiteSpace(kvp.Key)) { continue; } var key = kvp.Key.Trim(); if (!seen.Add(key)) { throw new ArgumentException(_t("common.audit.replay_token_duplicate_context_key", key), nameof(values)); } normalized.Add(new KeyValuePair(key, kvp.Value?.Trim() ?? string.Empty)); } return normalized .OrderBy(static kvp => kvp.Key, StringComparer.Ordinal) .ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.Ordinal); } /// /// Produces deterministic canonical representation of inputs. /// private static string Canonicalize(ReplayTokenRequest request, string version) { var canonical = new CanonicalReplayInput { Version = version, FeedManifests = NormalizeSortedList(request.FeedManifests), RulesVersion = NormalizeValue(request.RulesVersion), RulesHash = NormalizeValue(request.RulesHash), LatticePolicyVersion = NormalizeValue(request.LatticePolicyVersion), LatticePolicyHash = NormalizeValue(request.LatticePolicyHash), InputHashes = NormalizeSortedList(request.InputHashes), ScoringConfigVersion = NormalizeValue(request.ScoringConfigVersion), EvidenceHashes = NormalizeSortedList(request.EvidenceHashes), AdditionalContext = NormalizeSortedDictionary(request.AdditionalContext) }; return JsonSerializer.Serialize(canonical, _jsonOptions); } private sealed class CanonicalReplayInput { public required string Version { get; init; } public required List FeedManifests { get; init; } public string? RulesVersion { get; init; } public string? RulesHash { get; init; } public string? LatticePolicyVersion { get; init; } public string? LatticePolicyHash { get; init; } public required List InputHashes { get; init; } public string? ScoringConfigVersion { get; init; } public required List EvidenceHashes { get; init; } public required Dictionary AdditionalContext { get; init; } } }