diff --git a/dotnet/EcencyApi.Tests/JsValTests.cs b/dotnet/EcencyApi.Tests/JsValTests.cs index 3185e21e..c16b2125 100644 --- a/dotnet/EcencyApi.Tests/JsValTests.cs +++ b/dotnet/EcencyApi.Tests/JsValTests.cs @@ -49,6 +49,27 @@ public void NumberCoerce_MatchesJs(string input, double expected) else Assert.Equal(expected, actual); } + [Fact] + public void LoneSurrogates_ExtractAndSerializeLikeJs() + { + // JSON.parse accepts lone surrogate escapes (a JS string is arbitrary + // UTF-16); System.Text.Json throws InvalidOperationException when + // materializing such strings, which turned payloads the Node service + // handled fine into 500s. The lenient path must extract them, and + // Stringify must re-emit the escape like well-formed JSON.stringify. + var node = JsonNode.Parse("{\"app\":\"x\\ud83dy\"}")!; + + var s = JsVal.AsString(node["app"]); + Assert.NotNull(s); + Assert.Equal(3, s!.Length); + Assert.Equal('\ud83d', s[1]); + + Assert.Equal("{\"app\":\"x\\ud83dy\"}", JsJson.Stringify(node)); + + // ToJsString (template-literal coercion) must survive it too + Assert.Equal("x\ud83dy", JsVal.ToJsString(node["app"])); + } + [Fact] public void ToJsString_CoercesLikeJs() { diff --git a/dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json b/dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json index d8d0e6f2..741a456d 100644 --- a/dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json +++ b/dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json @@ -1470,6 +1470,11 @@ } ], "validateCodeRaw": [ + { + "tokenJson": "{\"signed_message\":{\"type\":\"code\",\"app\":\"x\\ud83dy\"},\"authors\":[\"mallory\"],\"timestamp\":1752300000,\"signatures\":[\"ab\"]}", + "rawMessage": "{\"signed_message\":{\"type\":\"code\",\"app\":\"x\\ud83dy\"},\"authors\":[\"mallory\"],\"timestamp\":1752300000}", + "digestHex": "6162f04277a85ad43b614e076d81a317ad873450ff71176ff4715ea0844b5e78" + }, { "tokenJson": "{\"signed_message\":{\"type\":\"code\",\"app\":\"ecency.app\"},\"authors\":[\"alice\"],\"timestamp\":1751900000,\"signatures\":[\"ab\"]}", "rawMessage": "{\"signed_message\":{\"type\":\"code\",\"app\":\"ecency.app\"},\"authors\":[\"alice\"],\"timestamp\":1751900000}", diff --git a/dotnet/EcencyApi/Handlers/AuthApi.cs b/dotnet/EcencyApi/Handlers/AuthApi.cs index 2976bf2d..6f68de7b 100644 --- a/dotnet/EcencyApi/Handlers/AuthApi.cs +++ b/dotnet/EcencyApi/Handlers/AuthApi.cs @@ -60,7 +60,7 @@ private static string JsToString(JsonNode? node) // Array.prototype.toString: elements joined with ",", null -> "" return string.Join(",", arr.Select(e => e == null ? "" : JsToString(e))); case JsonValue v: - if (v.TryGetValue(out var s)) return s; + if (JsVal.TryGetStringLenient(v, out var s)) return s; if (v.TryGetValue(out var b)) return b ? "true" : "false"; return JsJson.Stringify(v); default: diff --git a/dotnet/EcencyApi/Handlers/HiveExplorer.cs b/dotnet/EcencyApi/Handlers/HiveExplorer.cs index 094d6b61..ce4a0a14 100644 --- a/dotnet/EcencyApi/Handlers/HiveExplorer.cs +++ b/dotnet/EcencyApi/Handlers/HiveExplorer.cs @@ -128,7 +128,7 @@ public static double ParseToken(JsonNode? val) return 0d; } - if (val is not JsonValue jv || !jv.TryGetValue(out var s)) + if (val is not JsonValue jv || !JsVal.TryGetStringLenient(jv, out var s)) { return 0d; } @@ -166,7 +166,7 @@ private static double ParseFloatJs(string s) // TypeError), which the outer catch rewrites to "Failed to get globalProps". private static double GetVestAmount(JsonNode? value) { - if (value is JsonValue jv && jv.TryGetValue(out _)) + if (value is JsonValue jv && JsVal.TryGetStringLenient(jv, out _)) { return ParseToken(value); } @@ -238,7 +238,7 @@ private static double JsNumber(JsonNode? node) { return double.NaN; // "true"/"false" don't parse } - if (ev.TryGetValue(out var es)) + if (JsVal.TryGetStringLenient(ev, out var es)) { return JsNumberString(es); } @@ -252,7 +252,7 @@ private static double JsNumber(JsonNode? node) { return b ? 1d : 0d; } - if (value.TryGetValue(out var str)) + if (JsVal.TryGetStringLenient(value, out var str)) { return JsNumberString(str); } diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs b/dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs index 7a853a18..2714583d 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs @@ -69,7 +69,7 @@ public static string SignupClientIp(HttpContext ctx) private static async Task VerifyTurnstile(JsonNode? token, string ip) { var secret = Config.TurnstileSecret; - var tokenStr = token is JsonValue tv && tv.TryGetValue(out string? ts) ? ts : null; + var tokenStr = token is JsonValue tv && JsVal.TryGetStringLenient(tv, out string? ts) ? ts : null; if (string.IsNullOrEmpty(secret) || tokenStr == null || tokenStr.Trim().Length == 0) { return false; diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs b/dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs index b2695a5a..0b80eb87 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs @@ -259,7 +259,7 @@ private static string RpcErrorMessage(JsonNode? errorNode, string fallback) private static string ParseHexBalance(JsonNode? value) { - if (value is not JsonValue jsonValue || !jsonValue.TryGetValue(out var text)) + if (value is not JsonValue jsonValue || !JsVal.TryGetStringLenient(jsonValue, out var text)) { throw new Exception("Invalid hexadecimal balance response"); } @@ -595,7 +595,7 @@ private static async Task GetBlockstreamAccessToken() var data = resp.BodyIsJson ? resp.Json : JsonValue.Create(resp.RawText); var obj = data as JsonObject; - var accessToken = obj?["access_token"] is JsonValue tokenValue && tokenValue.TryGetValue(out var tokenText) + var accessToken = obj?["access_token"] is JsonValue tokenValue && JsVal.TryGetStringLenient(tokenValue, out var tokenText) ? tokenText : null; @@ -1107,7 +1107,7 @@ public static async Task ChainRpc(HttpContext ctx) null => "null", JsonObject => "[object Object]", JsonArray arr => string.Join(",", arr.Select(ChainJsString)), - JsonValue v when v.TryGetValue(out var s) => s, + JsonValue v when JsVal.TryGetStringLenient(v, out var s) => s, _ => JsJson.Stringify(node), }; diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs b/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs index a0b1c33f..67f9da08 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs @@ -37,7 +37,7 @@ public static partial class PrivateApi { var hasCode = body.TryGetPropertyValue("code", out var codeNode); string? code = null; - if (codeNode is JsonValue codeValue && codeValue.TryGetValue(out var codeStr)) + if (codeNode is JsonValue codeValue && JsVal.TryGetStringLenient(codeValue, out var codeStr)) { code = codeStr; } @@ -98,7 +98,7 @@ public static partial class PrivateApi string? author = null; if (authors is { Count: > 0 } && authors[0] is JsonValue authorValue - && authorValue.TryGetValue(out var authorStr)) + && JsVal.TryGetStringLenient(authorValue, out var authorStr)) { author = authorStr; } @@ -109,7 +109,7 @@ timestampNode is JsonValue timestampValue string? signature = null; if (signatures is { Count: > 0 } && signatures[0] is JsonValue signatureValue - && signatureValue.TryGetValue(out var signatureStr)) + && JsVal.TryGetStringLenient(signatureValue, out var signatureStr)) { signature = signatureStr; } @@ -210,7 +210,7 @@ timestampNode is JsonValue timestampValue } var first = item[0]; // throws for non-indexable nodes, like JS destructuring a non-iterable - keys.Add(first is JsonValue v && v.TryGetValue(out var key) ? key : null); + keys.Add(first is JsonValue v && JsVal.TryGetStringLenient(v, out var key) ? key : null); } return keys; diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.Feeds.cs b/dotnet/EcencyApi/Handlers/PrivateApi.Feeds.cs index 56228986..d2fe4f44 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.Feeds.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.Feeds.cs @@ -297,7 +297,7 @@ private static string FeedsJsToString(JsonNode? node) JsonObject => "[object Object]", _ => FeedsJsToString(e), })); - case JsonValue v when v.TryGetValue(out var s): + case JsonValue v when JsVal.TryGetStringLenient(v, out var s): return s; case JsonValue v when v.TryGetValue(out var b): return b ? "true" : "false"; diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs b/dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs index 09e2344f..8fb5e5f9 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs @@ -27,7 +27,7 @@ private static string MiscJsString(JsonNode? node) } if (node is JsonValue v) { - if (v.TryGetValue(out var s)) + if (JsVal.TryGetStringLenient(v, out var s)) { return s; } diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.Payments.cs b/dotnet/EcencyApi/Handlers/PrivateApi.Payments.cs index 68ecef40..d7264a97 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.Payments.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.Payments.cs @@ -148,7 +148,7 @@ public static async Task StripeCreateIntent(HttpContext ctx) // Reject a malformed value at this boundary before forwarding (typeof guard first: // a non-string must never be coerced by the regex test and forwarded). var hostingTargetPresent = body.TryGetPropertyValue("hosting_target", out var hostingTargetNode); - var hostingTarget = hostingTargetNode is JsonValue htv && htv.TryGetValue(out var hts) + var hostingTarget = hostingTargetNode is JsonValue htv && JsVal.TryGetStringLenient(htv, out var hts) ? hts : null; if (hostingTargetPresent && (hostingTarget == null || !PaymentsHiveNameRegex.IsMatch(hostingTarget))) @@ -160,7 +160,7 @@ public static async Task StripeCreateIntent(HttpContext ctx) // lowercase, so normalize typed input (trim + lowercase) before validating so a // recipient like "Alice" is accepted as "alice", and forward the canonical form. var giftRecipientPresent = body.TryGetPropertyValue("gift_recipient", out var giftRecipientNode); - var giftRecipient = giftRecipientNode is JsonValue grv && grv.TryGetValue(out var grs) + var giftRecipient = giftRecipientNode is JsonValue grv && JsVal.TryGetStringLenient(grv, out var grs) ? grs.Trim().ToLowerInvariant() : null; if (giftRecipientPresent && (giftRecipient == null || !PaymentsHiveNameRegex.IsMatch(giftRecipient))) @@ -169,7 +169,7 @@ public static async Task StripeCreateIntent(HttpContext ctx) return; } var giftMessagePresent = body.TryGetPropertyValue("gift_message", out var giftMessageNode); - var giftMessage = giftMessageNode is JsonValue gmv && gmv.TryGetValue(out var gms) + var giftMessage = giftMessageNode is JsonValue gmv && JsVal.TryGetStringLenient(gmv, out var gms) ? gms : null; if (giftMessagePresent && (giftMessage == null || giftMessage.Length > 200)) @@ -392,7 +392,7 @@ private static string PaymentsSignupClientIp(HttpContext ctx) private static async Task PaymentsVerifyTurnstile(JsonNode? token, string ip) { var secret = Config.TurnstileSecret; - var tokenStr = token is JsonValue tv && tv.TryGetValue(out var ts) ? ts : null; + var tokenStr = token is JsonValue tv && JsVal.TryGetStringLenient(tv, out var ts) ? ts : null; if (string.IsNullOrEmpty(secret) || tokenStr == null || tokenStr.Trim().Length == 0) { return false; @@ -474,7 +474,7 @@ private static string PaymentsJsToString(JsonNode? node) case JsonObject: return "[object Object]"; case JsonValue value: - if (value.TryGetValue(out var s)) + if (JsVal.TryGetStringLenient(value, out var s)) { return s; } diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.UserData1.cs b/dotnet/EcencyApi/Handlers/PrivateApi.UserData1.cs index 0898ced0..445453de 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.UserData1.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.UserData1.cs @@ -405,7 +405,7 @@ public static string Template(JsonNode? node) // Array.prototype.toString: elements joined by ",", holes/null -> "" return string.Join(",", arr.Select(item => item == null ? "" : Template(item))); default: - if (node is JsonValue jv && jv.TryGetValue(out var s)) + if (node is JsonValue jv && JsVal.TryGetStringLenient(jv, out var s)) { return s; } diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.UserData2.cs b/dotnet/EcencyApi/Handlers/PrivateApi.UserData2.cs index ccde5fed..2bf92ee8 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.UserData2.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.UserData2.cs @@ -328,7 +328,7 @@ private static string JsString(JsonNode? node) // Array.prototype.toString: join(","), null/undefined elements -> "" return string.Join(",", arr.Select(el => el == null ? "" : JsString(el))); case JsonValue v: - if (v.TryGetValue(out var s)) return s; + if (JsVal.TryGetStringLenient(v, out var s)) return s; if (v.TryGetValue(out var b)) return b ? "true" : "false"; // numbers: String(n) == JSON.stringify(n) for finite doubles return JsJson.Stringify(node); diff --git a/dotnet/EcencyApi/Handlers/SearchApi.cs b/dotnet/EcencyApi/Handlers/SearchApi.cs index 056d9c7a..10767e31 100644 --- a/dotnet/EcencyApi/Handlers/SearchApi.cs +++ b/dotnet/EcencyApi/Handlers/SearchApi.cs @@ -54,7 +54,7 @@ private static string JsToString(JsonNode? node) // Array.prototype.toString: elements joined with ",", null -> "" return string.Join(",", arr.Select(e => e == null ? "" : JsToString(e))); case JsonValue v: - if (v.TryGetValue(out var s)) return s; + if (JsVal.TryGetStringLenient(v, out var s)) return s; if (v.TryGetValue(out var b)) return b ? "true" : "false"; return JsJson.Stringify(v); default: diff --git a/dotnet/EcencyApi/Infrastructure/ApiClient.cs b/dotnet/EcencyApi/Infrastructure/ApiClient.cs index 1034a6fd..95d1fefc 100644 --- a/dotnet/EcencyApi/Infrastructure/ApiClient.cs +++ b/dotnet/EcencyApi/Infrastructure/ApiClient.cs @@ -43,7 +43,7 @@ public ApiAuthException() : base("Api auth couldn't be create!") { } { headers[kv.Key] = kv.Value switch { - JsonValue v when v.TryGetValue(out var s) => s, + JsonValue v when JsVal.TryGetStringLenient(v, out var s) => s, null => "null", _ => kv.Value.ToJsonString(), }; diff --git a/dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs b/dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs index 4c7411e2..48ea07af 100644 --- a/dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs +++ b/dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs @@ -92,7 +92,7 @@ public static async Task SendJson(this HttpContext ctx, int status, JsonNode? no /// Convenience: string body field (undefined -> null). public static string? Str(this JsonObject body, string key) => - body.TryGetPropertyValue(key, out var v) && v is JsonValue val && val.TryGetValue(out var s) + body.TryGetPropertyValue(key, out var v) && v is JsonValue val && JsVal.TryGetStringLenient(val, out var s) ? s : null; diff --git a/dotnet/EcencyApi/Infrastructure/JsJson.cs b/dotnet/EcencyApi/Infrastructure/JsJson.cs index f1f571d5..1ffa0efb 100644 --- a/dotnet/EcencyApi/Infrastructure/JsJson.cs +++ b/dotnet/EcencyApi/Infrastructure/JsJson.cs @@ -63,7 +63,7 @@ private static void WriteValue(StringBuilder sb, JsonNode? node) private static void WritePrimitive(StringBuilder sb, JsonValue value) { - if (value.TryGetValue(out var s)) + if (JsVal.TryGetStringLenient(value, out var s)) { WriteString(sb, s); return; @@ -260,7 +260,7 @@ public static bool IsTruthy(JsonNode? node) case JsonArray: return true; case JsonValue v: - if (v.TryGetValue(out var s)) return s.Length > 0; + if (JsVal.TryGetStringLenient(v, out var s)) return s.Length > 0; if (v.TryGetValue(out var b)) return b; // Parsed values are element-backed; programmatically built ones // are CLR-backed and would throw on GetValue(). diff --git a/dotnet/EcencyApi/Infrastructure/JsVal.cs b/dotnet/EcencyApi/Infrastructure/JsVal.cs index 32d3afeb..caf765be 100644 --- a/dotnet/EcencyApi/Infrastructure/JsVal.cs +++ b/dotnet/EcencyApi/Infrastructure/JsVal.cs @@ -19,7 +19,7 @@ public static class JsVal public static bool IsString(JsonNode? n) => n is JsonValue v && v.TryGetValue(out var e) ? e.ValueKind == JsonValueKind.String - : n is JsonValue v2 && v2.TryGetValue(out _); + : n is JsonValue v2 && JsVal.TryGetStringLenient(v2, out _); public static bool IsNumber(JsonNode? n) { @@ -41,12 +41,77 @@ public static bool IsBool(JsonNode? n) // ---- accessors ------------------------------------------------------- + /// + /// String extraction tolerant of lone-surrogate \u escapes. JavaScript's + /// JSON.parse accepts them (JS strings are arbitrary UTF-16), but + /// System.Text.Json throws InvalidOperationException when materializing + /// such strings — which turned payloads the Node service handled fine into + /// 500s. Falls back to manually unescaping the raw JSON text with JS + /// semantics (\uXXXX decodes to the code unit, paired or not). + /// + public static bool TryGetStringLenient(JsonValue v, out string value) + { + try + { + if (v.TryGetValue(out value!)) + { + return true; + } + } + catch (InvalidOperationException) + { + // incomplete surrogate pair in the JSON text; fall through + } + + if (v.TryGetValue(out var el) && el.ValueKind == JsonValueKind.String) + { + value = UnescapeJsonStringLenient(el.GetRawText()); + return true; + } + + value = null!; + return false; + } + + /// Unescape a raw JSON string literal (including quotes) with JS semantics. + internal static string UnescapeJsonStringLenient(string raw) + { + var sb = new StringBuilder(raw.Length); + for (var i = 1; i < raw.Length - 1; i++) + { + var c = raw[i]; + if (c != '\\') + { + sb.Append(c); + continue; + } + i++; + switch (raw[i]) + { + case '"': sb.Append('"'); break; + case '\\': sb.Append('\\'); break; + case '/': sb.Append('/'); break; + case 'b': sb.Append('\b'); break; + case 'f': sb.Append('\f'); break; + case 'n': sb.Append('\n'); break; + case 'r': sb.Append('\r'); break; + case 't': sb.Append('\t'); break; + case 'u': + sb.Append((char)ushort.Parse(raw.AsSpan(i + 1, 4), + NumberStyles.HexNumber, CultureInfo.InvariantCulture)); + i += 4; + break; + } + } + return sb.ToString(); + } + /// The JSON string value, or null when the node is not a string. public static string? AsString(JsonNode? n) { if (n is JsonValue v) { - if (v.TryGetValue(out var s)) return s; + if (JsVal.TryGetStringLenient(v, out var s)) return s; if (v.TryGetValue(out var e) && e.ValueKind == JsonValueKind.String) return e.GetString(); } @@ -180,7 +245,7 @@ public static string ToJsString(JsonNode? n) case JsonObject: return "[object Object]"; case JsonValue v: - if (v.TryGetValue(out var s)) return s; + if (JsVal.TryGetStringLenient(v, out var s)) return s; if (v.TryGetValue(out var b)) return b ? "true" : "false"; var num = AsNumber(n); if (num.HasValue) return JsNumberToString(num.Value); diff --git a/dotnet/EcencyApi/Infrastructure/Upstream.cs b/dotnet/EcencyApi/Infrastructure/Upstream.cs index 77cd8537..53df0668 100644 --- a/dotnet/EcencyApi/Infrastructure/Upstream.cs +++ b/dotnet/EcencyApi/Infrastructure/Upstream.cs @@ -203,7 +203,7 @@ public static async Task Pipe(Task upstreamTask, HttpContext c } catch (UpstreamTimeoutException e) { - Console.Error.WriteLine($"pipe(): upstream timeout: {e.Message}"); + Console.Error.WriteLine($"pipe(): upstream timeout on {ctx.Request.Path}: {e.Message}"); if (!ctx.Response.HasStarted) { ctx.Response.StatusCode = 504; @@ -213,7 +213,7 @@ public static async Task Pipe(Task upstreamTask, HttpContext c } catch (Exception e) { - Console.Error.WriteLine($"pipe(): error while processing API call: {e.Message}"); + Console.Error.WriteLine($"pipe(): error on {ctx.Request.Path}: {e.Message}"); if (!ctx.Response.HasStarted) { ctx.Response.StatusCode = 500; @@ -224,7 +224,7 @@ public static async Task Pipe(Task upstreamTask, HttpContext c if (ctx.Response.HasStarted) { - Console.Error.WriteLine("pipe(): headers already sent, skipping response"); + Console.Error.WriteLine($"pipe(): headers already sent, skipping response ({ctx.Request.Path})"); return; } @@ -266,7 +266,10 @@ public static async Task SendLikeExpress(HttpContext ctx, int status, JsonNode? case JsonValueKind.String: // res.send("str") sends the raw string as text/html ctx.Response.ContentType = "text/html; charset=utf-8"; - await ctx.Response.WriteAsync(value.GetValue()); + if (JsVal.TryGetStringLenient(value, out var bodyStr)) + { + await ctx.Response.WriteAsync(bodyStr); + } return; } } diff --git a/dotnet/tools/gen-vectors.js b/dotnet/tools/gen-vectors.js index 3f14c6dc..b4874e2e 100644 --- a/dotnet/tools/gen-vectors.js +++ b/dotnet/tools/gen-vectors.js @@ -97,6 +97,9 @@ for (const [l, app, timestamp] of [ // b64u-encode it) -> the exact rawMessage Node re-serializes and hashes. // Exercises nested key-order preservation, fractional timestamps, unicode. const tokenJsons = [ + // lone surrogate escape: JSON.parse accepts it; the port must too (and + // re-serialize it as the same escape, per well-formed JSON.stringify) + '{"signed_message":{"type":"code","app":"x\\ud83dy"},"authors":["mallory"],"timestamp":1752300000,"signatures":["ab"]}', '{"signed_message":{"type":"code","app":"ecency.app"},"authors":["alice"],"timestamp":1751900000,"signatures":["ab"]}', '{"signed_message":{"app":"ecency.app","type":"code"},"authors":["bob.tester"],"timestamp":1751900000.123,"signatures":["cd"]}', '{"timestamp":1700000000.5,"signatures":["ef"],"signed_message":{"type":"login","app":"éçency 漢字"},"authors":["carol-x1"]}',