using System.Globalization; using System.Text; namespace Nexus.Api.Repositories; internal readonly record struct OpenClawRunCursorPosition( DateTimeOffset CreatedAt, Guid? Id); internal static class OpenClawRunCursorCodec { private const string Version = "v1"; private const int MaximumEncodedLength = 128; public static string Encode(DateTimeOffset createdAt, Guid id) { var payload = string.Create( CultureInfo.InvariantCulture, $"{Version}|{createdAt.UtcTicks}|{id:N}"); return Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } public static bool TryDecode( string? cursor, out OpenClawRunCursorPosition position) { position = default; if (string.IsNullOrWhiteSpace(cursor) || cursor.Length > MaximumEncodedLength) return false; // Compatibility with the original run cursor, which was an unversioned // UTC tick value and therefore cannot express the Id tie-breaker. if (long.TryParse( cursor, NumberStyles.None, CultureInfo.InvariantCulture, out var legacyTicks)) { return TryCreatePosition(legacyTicks, null, out position); } try { var normalized = cursor .Replace('-', '+') .Replace('_', '/'); normalized = (normalized.Length % 4) switch { 0 => normalized, 2 => normalized + "==", 3 => normalized + "=", _ => throw new FormatException("Invalid Base64Url length.") }; var payload = new UTF8Encoding( encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true) .GetString(Convert.FromBase64String(normalized)); var parts = payload.Split('|'); if (parts.Length != 3 || !string.Equals(parts[0], Version, StringComparison.Ordinal) || !long.TryParse( parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var utcTicks) || !Guid.TryParseExact(parts[2], "N", out var id)) { return false; } return TryCreatePosition(utcTicks, id, out position); } catch (Exception exception) when ( exception is FormatException or DecoderFallbackException) { return false; } } private static bool TryCreatePosition( long utcTicks, Guid? id, out OpenClawRunCursorPosition position) { position = default; try { position = new OpenClawRunCursorPosition( new DateTimeOffset(utcTicks, TimeSpan.Zero), id); return true; } catch (ArgumentOutOfRangeException) { return false; } } }