using System.Text.Json; namespace ZB.MOM.WW.Auth.ApiKeys.Sqlite; /// /// Serializes API-key scope sets to a canonical JSON array. Scopes are sorted with /// so that equal sets always produce identical /// column text, regardless of insertion order. /// public static class ScopeSerializer { /// Serializes scopes to an ordinal-sorted JSON array. /// The scopes to serialize. /// A JSON array string with elements sorted ordinally. public static string Serialize(IReadOnlySet scopes) { ArgumentNullException.ThrowIfNull(scopes); return JsonSerializer.Serialize(scopes.Order(StringComparer.Ordinal)); } /// Deserializes scopes from a JSON array string. /// The JSON string to deserialize; may be null or empty. /// /// An ordinal-compared set of scopes; empty when the input is null/blank. A malformed or /// non-array column (operator tampering, a partial write, a format change, or a buggy writer) /// fails closed to an EMPTY set rather than throwing, so a single poisoned row degrades to a /// zero-scope identity on the auth path instead of an unhandled . /// public static IReadOnlySet Deserialize(string? value) { if (string.IsNullOrWhiteSpace(value)) { return new HashSet(StringComparer.Ordinal); } string[]? scopes; try { scopes = JsonSerializer.Deserialize(value); } catch (JsonException) { // Fail closed: a corrupt scopes column yields no scopes rather than an exception on the // verification hot path. The verifier's "only exception path is cancellation" contract // is preserved, and a key with an unreadable scope set is left with zero authority. return new HashSet(StringComparer.Ordinal); } return new HashSet(scopes ?? [], StringComparer.Ordinal); } }