on master help sign in
Move validation into refined types to simplify handlers
commitd7ac42344f5ef92732340ed0535c8645aadbc2c2 authoriko <mail@iko.soy> dateWed 8 Apr 2026, 15:22 (6 months ago) parentb018bb7
32 files changed+121−158
client/ApiClient.fs+6−19 client/App.fs+21−16 client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.dll client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.pdb client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/_CodeSignature/CodeResources+4−4 client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.dll client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.pdb client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/_CodeSignature/CodeResources+2−2 client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfo.fs+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfoInputs.cache+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt−22 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.sourcelink.json+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/linked/Skrepka.dll client/obj/Debug/net10.0-ios/iossimulator-arm64/linked/Skrepka.pdb client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll client/obj/Debug/net10.0-ios/iossimulator-arm64/refint/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfo.fs+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfoInputs.cache+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.sourcelink.json+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/linked/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/linked/Skrepka.pdb client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/ref/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/refint/Skrepka.dll server.knot+82−89
client/ApiClient.fs+6−19
@@ -78,10 +78,10 @@ module ApiClient =
7878
7979 let private pollClient =
8080 let c = new HttpClient()
81− c.Timeout <- System.Threading.Timeout.InfiniteTimeSpan
81+ c.Timeout <- TimeSpan.FromSeconds(120.)
8282 c
8383
84− let poll (serverUrl: string) (token: string) (cursor: int64) =
84+ let poll (serverUrl: string) (token: string) (cursor: uint64) =
8585 async {
8686 let request = new HttpRequestMessage(HttpMethod.Post, $"{serverUrl}/poll")
8787 request.Content <- new StringContent($"""{{"cursor":{cursor},"timeout":30000}}""", Encoding.UTF8, "application/json")
@@ -91,29 +91,16 @@ module ApiClient =
9191 let! text = response.Content.ReadAsStringAsync() |> Async.AwaitTask
9292 let doc = JsonDocument.Parse(text)
9393 let root = expectObject doc "/poll"
94− let cursor = root.GetProperty("cursor").GetInt64()
94+ let cursor = root.GetProperty("cursor").GetUInt64
9595
96− // Knot runtime wraps relations as [[record1, record2, ...]]
97− let eventsOuter = root.GetProperty("events")
98−
99− let eventsInner =
100− if eventsOuter.GetArrayLength() > 0
101− && eventsOuter.[0].ValueKind = JsonValueKind.Array then
102− eventsOuter.[0]
103− else
104− eventsOuter
96+ let events= root.GetProperty("events")
10597
10698 let events =
107− [ for e in eventsInner.EnumerateArray() do
99+ [ for e in events.EnumerateArray() do
108100 if e.ValueKind = JsonValueKind.Object then
109101 let id = e.GetProperty("id").GetString()
110102 let evtType = e.GetProperty("eventType").GetString()
111− let payloadEl = e.GetProperty("payload")
112− let payload =
113− if payloadEl.ValueKind = JsonValueKind.String then
114− payloadEl.GetString()
115− else
116− payloadEl.GetRawText()
103+ let payload = e.GetProperty("payload")
117104 yield (id, evtType, payload) ]
118105
119106 return (events, cursor)
client/App.fs+21−16
@@ -47,8 +47,9 @@ module App =
4747 NewPubkey: string
4848 NewNickname: string
4949 Error: string option
50− PollCursor: int64
51− LastSendStatus: string }
50+ PollCursor: uint64
51+ LastSendStatus: string
52+ Events: list<JsonElement> }
5253
5354 // ── Msg ──
5455
@@ -68,7 +69,7 @@ module App =
6869 | SetNewNickname of string
6970 | DoSaveContact
7071 | DoDeleteContact of string
71− | PollResult of (string * string * int64 * string) list * string * int64
72+ | PollResult of (string * string * uint64 * string) list * string * uint64 * list<JsonElement>
7273 | DoPoll
7374 | CopyPubKey
7475 | DismissError
@@ -86,7 +87,7 @@ module App =
8687 pubKeyHex: string *
8788 recipPubHex: string *
8889 text: string
89− | CmdPoll of url: string * token: string * privKey: byte[] * cursor: int64
90+ | CmdPoll of url: string * token: string * privKey: byte[] * cursor: uint64
9091 | CmdCopyToClipboard of string
9192
9293 // ── Helpers ──
@@ -115,8 +116,9 @@ module App =
115116 NewPubkey = ""
116117 NewNickname = ""
117118 Error = None
118− PollCursor = 0L
119− LastSendStatus = "" },
119+ PollCursor = 0UL
120+ LastSendStatus = ""
121+ Events = [] },
120122 []
121123
122124 // ── Update ──
@@ -174,7 +176,7 @@ module App =
174176
175177 | Sent(_, status) -> { model with LastSendStatus = status }, []
176178
177− | PollResult(incoming, status, newCursor) ->
179+ | PollResult(incoming, status, newCursor, events) ->
178180 let model' =
179181 incoming
180182 |> List.fold
@@ -199,7 +201,7 @@ module App =
199201 { m with
200202 Contacts = contacts
201203 Messages = m.Messages |> Map.add fromPk (msgs @ [ newMsg ]) })
202− { model with PollStatus = status; PollCursor = newCursor }
204+ { model with PollStatus = status; PollCursor = newCursor; Events = model.Events @ events }
203205
204206 match model'.Conn with
205207 | Online token -> model', [ CmdPoll(model'.ServerUrl, token, model'.PrivKey.Value, model'.PollCursor) ]
@@ -309,14 +311,13 @@ module App =
309311 for (evtId, evtType, payload) in events do
310312 if evtType = "message" then
311313 try
312− let doc = JsonDocument.Parse(payload)
313− let blobHex = doc.RootElement.GetProperty("encryptedBlob").GetString()
314− let tsEl = doc.RootElement.GetProperty("timestamp")
314+ let blobHex = payload.GetProperty("encryptedBlob").GetString()
315+ let tsEl = payload.GetProperty("timestamp")
315316 let ts =
316317 if tsEl.ValueKind = JsonValueKind.String then
317− Int64.Parse(tsEl.GetString())
318+ UInt64.Parse(tsEl.GetString())
318319 else
319− tsEl.GetInt64()
320+ tsEl.GetUInt64()
320321 let blob = Crypto.fromHex blobHex
321322
322323 match Crypto.decrypt privKey blob with
@@ -344,15 +345,15 @@ module App =
344345 $"[{now}] evts:{events.Length} msgs:{messages.Count} errs:{errors.Count}"
345346 + (if errors.Count > 0 then $" [{errors.[0]}]" else "")
346347
347− return PollResult(Seq.toList messages, status, newCursor)
348+ return PollResult(Seq.toList messages, status, newCursor, events |> List.map (fun (_, _, x) -> x))
348349 with
349350 | :? System.Threading.Tasks.TaskCanceledException ->
350351 // Normal timeout from long poll — just retry
351− return PollResult([], "polling...", cursor)
352+ return PollResult([], "polling...", cursor, [])
352353 | ex ->
353354 let now = DateTimeOffset.UtcNow.ToString("HH:mm:ss")
354355 do! Async.Sleep 3000
355− return PollResult([], $"[{now}] poll error: {ex.Message}", cursor)
356+ return PollResult([], $"[{now}] poll error: {ex.Message}", cursor, [])
356357 })
357358
358359 | CmdCopyToClipboard text ->
@@ -450,6 +451,10 @@ module App =
450451 Label($"poll: {model.PollStatus}")
451452 .font(size = 10.)
452453 .textColor(Colors.DimGray)
454+
455+ Label($"Events: {model.Events}")
456+ .font(10)
457+ .textColor(Colors.DimGray)
453458
454459 if contactNames <> "" then
455460 Label($"names: {contactNames}")
client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/_CodeSignature/CodeResources+4−4
@@ -162,11 +162,11 @@
162162 </data>
163163 <key>Skrepka.dll</key>
164164 <data>
165− ypJ+yfPZXg7YJryVefkXlKup+Nk=
165+ di0VTroRrk0Osy30exH9yUkakk8=
166166 </data>
167167 <key>Skrepka.pdb</key>
168168 <data>
169− 4a8oElSLOag9iakfuSuDnBn6OSY=
169+ /Vq3uwq4IB+ENxjl99BAiVqJSyI=
170170 </data>
171171 <key>Sodium.Core.dll</key>
172172 <data>
@@ -1382,14 +1382,14 @@
13821382 <dict>
13831383 <key>hash2</key>
13841384 <data>
1385− 3HIejp0Oax8kuHte26tQAF8ZpWxb4PH7Rav4jj36Fdo=
1385+ TzHV+4wEf6Ct4OfAEhd4ZU3QwzVFyBaCT50Uet/ckAA=
13861386 </data>
13871387 </dict>
13881388 <key>Skrepka.pdb</key>
13891389 <dict>
13901390 <key>hash2</key>
13911391 <data>
1392− qfGf9JRhxOL/rS+pxNyVJYORTjOcPe0VeyDVNiUixm0=
1392+ AR+okdAV5ZOA0Rj5RzoaFK5GZ4bSMLlz0J92sjaMxdY=
13931393 </data>
13941394 </dict>
13951395 <key>Sodium.Core.dll</key>
client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/_CodeSignature/CodeResources+2−2
@@ -240,14 +240,14 @@
240240 <dict>
241241 <key>hash2</key>
242242 <data>
243− IgRsNtHUh8DMz++3XNrUGDQZZ5TZgu/bD9nA2tliTFs=
243+ fLiyHbdfTDvK9D25CmTS2vrZbxslf5o37h7stAFyyzc=
244244 </data>
245245 </dict>
246246 <key>MonoBundle/Skrepka.pdb</key>
247247 <dict>
248248 <key>hash2</key>
249249 <data>
250− xfWT1LzLa2DN2zoKTnJbHK1R5yU1ojDib6FF9PkLSVM=
250+ UYV1Js3NxCMbEhwDushUOfVNf2Zqfoff+tT+ijnSNAw=
251251 </data>
252252 </dict>
253253 <key>MonoBundle/Sodium.Core.dll</key>
client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfo.fs+1−1
@@ -10,7 +10,7 @@ open System.Reflection
1010[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
1111[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
1212[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+a2e3a5ec77526b2266813abd90e19cc8da79ea95")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+b018bb70ad38e33f052804139428cc5ad37cf583")>]
1414[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
1515[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
1616[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfoInputs.cache+1−1
@@ -1 +1 @@
1−91db709e0ab300cde81b841e6cd902d57b7be13fdc88acacbfd6f68064f6c745
1+57e7ff8ad3251b11511c75e432d109c5c81fd656f2685113d1e9afa4e3a11850
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt−22
@@ -333,25 +333,3 @@
333333/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb
334334/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.genruntimeconfig.cache
335335/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll
336−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.stamp
337−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.MauiControls.stamp
338−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/FSharp.Core.stamp
339−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.Abstractions.stamp
340−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.stamp
341−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.Abstractions.stamp
342−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.stamp
343−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Diagnostics.Abstractions.stamp
344−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.FileProviders.Abstractions.stamp
345−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Hosting.Abstractions.stamp
346−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.Abstractions.stamp
347−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.stamp
348−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Options.stamp
349−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Primitives.stamp
350−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Compatibility.stamp
351−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.stamp
352−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Xaml.stamp
353−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.stamp
354−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Essentials.stamp
355−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Graphics.stamp
356−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Sodium.Core.stamp
357−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/unpack/Microsoft.Maui.Essentials/BundleResource/xml/microsoft_maui_essentials_fileprovider_file_paths.xml
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.sourcelink.json+1−1
@@ -1 +1 @@
1−{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/a2e3a5ec77526b2266813abd90e19cc8da79ea95/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/b018bb70ad38e33f052804139428cc5ad37cf583/*"}}
client/obj/Debug/net10.0-ios/iossimulator-arm64/linked/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/linked/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/refint/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfo.fs+1−1
@@ -10,7 +10,7 @@ open System.Reflection
1010[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
1111[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
1212[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+a2e3a5ec77526b2266813abd90e19cc8da79ea95")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+b018bb70ad38e33f052804139428cc5ad37cf583")>]
1414[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
1515[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
1616[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfoInputs.cache+1−1
@@ -1 +1 @@
1−bf2b07610dc50305bc228fc07164d577b79a6accf18e0d41a7ee67fcbc91f170
1+1884cb8a1baa514aa08abae9e9488933dc9303d202768ad3435ac63803776e48
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.sourcelink.json+1−1
@@ -1 +1 @@
1−{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/a2e3a5ec77526b2266813abd90e19cc8da79ea95/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/b018bb70ad38e33f052804139428cc5ad37cf583/*"}}
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/linked/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/linked/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/ref/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/refint/Skrepka.dll

Binary file not shown.

server.knot+82−89
@@ -40,10 +40,16 @@ maxForwardRetriesPerServer = 100
4040-- Types
4141-- ============================================
4242
43−type PollEvent = {id: Text, eventType: Text, payload: Text}
43+type PollEvent = {id: Text, eventType: Text, payload: {encryptedBlob: Text, fromKey: Text, timestamp: Int, toKey: Text}}
4444
4545type GossipEvent = {eventType: Text, pubkey: Text, server: Text, timestamp: Int, ttl: Int}
4646
47+type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen)
48+type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen)
49+type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen)
50+type MessageIdHex = Text where \s -> isValidHex s && not (tooLong s maxMessageIdLen)
51+type ServerName = Text where \s -> not (tooLong s maxServerNameLen) && not (isBadServerName s)
52+
4753-- ============================================
4854-- Persisted State
4955-- ============================================
@@ -70,10 +76,10 @@ type GossipEvent = {eventType: Text, pubkey: Text, server: Text, timestamp: Int,
7076
7177route AuthApi where
7278 /auth
73− POST {pubkey: Text} /challenge
79+ POST {pubkey: PubkeyHex} /challenge
7480 -> {challenge: Text}
7581 = ReqChallenge
76− POST {pubkey: Text, challenge: Text, signature: Text} /verify
82+ POST {pubkey: PubkeyHex, challenge: Text, signature: SignatureHex} /verify
7783 -> {token: Text, expiresAt: Int}
7884 = VerifyAuth
7985
@@ -82,25 +88,25 @@ route ClientApi where
8288 -> {events: [PollEvent], cursor: Int, authorized: Bool}
8389 = Poll
8490 /messages
85− POST {to: Text, encryptedBlob: Text, timestamp: Int} / headers {authorization: Text}
91+ POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Int} / headers {authorization: Text}
8692 -> {status: Text, messageId: Text}
8793 = SendMessage
88− POST {messageIds: [Text]} /ack headers {authorization: Text}
94+ POST {messageIds: [MessageIdHex]} /ack headers {authorization: Text}
8995 -> {ok: Bool}
9096 = AckMessages
91− GET /lookup/{pubkey: Text} headers {authorization: Text}
97+ GET /lookup/{pubkey: PubkeyHex} headers {authorization: Text}
9298 -> {online: Bool, server: Text, authorized: Bool}
9399 = LookupPresence
94100
95101route FederationApi where
96102 /federation
97− POST {events: [GossipEvent], hopsRemaining: Int, fromServer: Text} /gossip headers {authorization: Text}
103+ POST {events: [GossipEvent], hopsRemaining: Int, fromServer: ServerName} /gossip headers {authorization: Text}
98104 -> {ok: Bool}
99105 = RecvGossip
100− POST {toKey: Text, fromKey: Text, encryptedBlob: Text, timestamp: Int, originServer: Text, messageId: Text} /forward headers {authorization: Text}
106+ POST {toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Int, originServer: ServerName, messageId: MessageIdHex} /forward headers {authorization: Text}
101107 -> {ok: Bool}
102108 = ForwardMessage
103− POST {messageIds: [Text], originServer: Text, fromServer: Text} /ack headers {authorization: Text}
109+ POST {messageIds: [Text], originServer: Text, fromServer: ServerName} /ack headers {authorization: Text}
104110 -> {ok: Bool}
105111 = FederationAck
106112
@@ -563,6 +569,7 @@ sendAcksAndClean = \pendingAcks servers ->
563569 )
564570 sendAcksAndClean pendingAcks rest
565571
572+pollOnce : Text -> Int -> IO {clock} {authorized: Bool, cursor: Int, events: [PollEvent]}
566573pollOnce = \token cursor -> do
567574 t <- now
568575 atomic do
@@ -604,16 +611,16 @@ pollOnce = \token cursor -> do
604611 let evts = do
605612 m <- limitedMsgs
606613 yield {id: m.messageId, eventType: "message",
607− payload: toJson {toKey: m.toKey, fromKey: m.fromKey,
608− encryptedBlob: m.encryptedBlob,
609− timestamp: m.timestamp}}
614+ payload: {toKey: m.toKey, fromKey: m.fromKey,
615+ encryptedBlob: m.encryptedBlob,
616+ timestamp: m.timestamp}}
610617 let lastReceivedAt = fold (\acc m -> if m.receivedAt > acc then m.receivedAt else acc) clampedCursor limitedMsgs
611618 let newCursor = case session of
612619 Nothing {} -> -1
613620 _ -> lastReceivedAt
614621 yield {events: evts, cursor: newCursor, authorized: authorized}
615622
616−pollLoop : Text -> Int -> Int -> IO {clock} {authorized: Bool, cursor: Int, events: [{eventType: Text, id: Text, payload: Text}]}
623+pollLoop : Text -> Int -> Int -> IO {clock} {authorized: Bool, cursor: Int, events: [PollEvent]}
617624pollLoop = \token cursor deadline -> do
618625 result <- pollOnce token cursor
619626 t <- now
@@ -632,45 +639,41 @@ pollLoop = \token cursor deadline -> do
632639serve = \req -> case req of
633640
634641 ReqChallenge {pubkey, respond} -> do
635− let valid = not (pubkey == "") && not (tooLong pubkey maxPubkeyLen) && isValidHex pubkey
636− let pubkey = if isValidHex pubkey then normalizeHex pubkey else pubkey
642+ let pubkey = normalizeHex pubkey
637643 challenge <- genToken
638644 t <- now
639− if valid
640− then do
641− pruneResult <- atomic do
642− pr <- maybePrune t
643− challenges <- *challenges
644− let existingCount = count (do c <- challenges; where c.pubkey == pubkey; where c.expiresAt > t; yield c)
645− let oldestChallengeExpiry = fold (\acc c2 -> if c2.pubkey == pubkey && c2.expiresAt > t && (acc == -1 || c2.expiresAt < acc) then c2.expiresAt else acc) (-1) challenges
646− let cappedChallenges = if existingCount >= maxChallengesPerKey && oldestChallengeExpiry >= 0
647− then fold (\acc c ->
648− if not acc.removed && c.pubkey == pubkey && c.expiresAt == oldestChallengeExpiry
649− then {removed: True {}, items: acc.items}
650− else {removed: acc.removed, items: union acc.items [c]}) {removed: False {}, items: []} challenges
651− else {removed: False {}, items: challenges}
652− let withoutExpired = if count cappedChallenges.items >= maxChallenges
653− then do c <- cappedChallenges.items; where c.expiresAt > t; yield c
654− else cappedChallenges.items
655− let globalOldestExpiry = if count withoutExpired >= maxChallenges
656− then fold (\acc c -> if acc == -1 || c.expiresAt < acc then c.expiresAt else acc) (-1) withoutExpired
657− else -1
658− let globalCapped = if globalOldestExpiry >= 0
659− then (fold (\acc c ->
660− if not acc.removed && c.expiresAt == globalOldestExpiry
661− then {removed: True {}, items: acc.items}
662− else {removed: acc.removed, items: union acc.items [c]}) {removed: False {}, items: []} withoutExpired).items
663− else withoutExpired
664− set *challenges = union
665− globalCapped
666− [{pubkey: pubkey, challenge: challenge, expiresAt: t + challengeExpiry}]
667− yield pr
668− fork (broadcastOfflineGossip t pruneResult.peerServers pruneResult.expiredLocal)
669− yield (respond {challenge: challenge})
670− else yield (respond {challenge: ""})
645+ pruneResult <- atomic do
646+ pr <- maybePrune t
647+ challenges <- *challenges
648+ let existingCount = count (do c <- challenges; where c.pubkey == pubkey; where c.expiresAt > t; yield c)
649+ let oldestChallengeExpiry = fold (\acc c2 -> if c2.pubkey == pubkey && c2.expiresAt > t && (acc == -1 || c2.expiresAt < acc) then c2.expiresAt else acc) (-1) challenges
650+ let cappedChallenges = if existingCount >= maxChallengesPerKey && oldestChallengeExpiry >= 0
651+ then fold (\acc c ->
652+ if not acc.removed && c.pubkey == pubkey && c.expiresAt == oldestChallengeExpiry
653+ then {removed: True {}, items: acc.items}
654+ else {removed: acc.removed, items: union acc.items [c]}) {removed: False {}, items: []} challenges
655+ else {removed: False {}, items: challenges}
656+ let withoutExpired = if count cappedChallenges.items >= maxChallenges
657+ then do c <- cappedChallenges.items; where c.expiresAt > t; yield c
658+ else cappedChallenges.items
659+ let globalOldestExpiry = if count withoutExpired >= maxChallenges
660+ then fold (\acc c -> if acc == -1 || c.expiresAt < acc then c.expiresAt else acc) (-1) withoutExpired
661+ else -1
662+ let globalCapped = if globalOldestExpiry >= 0
663+ then (fold (\acc c ->
664+ if not acc.removed && c.expiresAt == globalOldestExpiry
665+ then {removed: True {}, items: acc.items}
666+ else {removed: acc.removed, items: union acc.items [c]}) {removed: False {}, items: []} withoutExpired).items
667+ else withoutExpired
668+ set *challenges = union
669+ globalCapped
670+ [{pubkey: pubkey, challenge: challenge, expiresAt: t + challengeExpiry}]
671+ yield pr
672+ fork (broadcastOfflineGossip t pruneResult.peerServers pruneResult.expiredLocal)
673+ yield (respond {challenge: challenge})
671674
672675 VerifyAuth {pubkey, challenge, signature, respond} -> do
673− let pubkey = if isValidHex pubkey then normalizeHex pubkey else pubkey
676+ let pubkey = normalizeHex pubkey
674677 t <- now
675678 token <- genToken
676679 let expiry = t + sessionExpiry
@@ -687,11 +690,7 @@ serve = \req -> case req of
687690 yield c)
688691 let sigValid = case matched of
689692 Nothing {} -> False {}
690− _ -> if tooLong signature maxSignatureLen
691− then False {}
692− else if not (isValidHex pubkey) || not (isValidHex signature)
693− then False {}
694− else verify (bytesFromHex pubkey) (textToBytes challenge) (bytesFromHex signature)
693+ _ -> verify (bytesFromHex pubkey) (textToBytes challenge) (bytesFromHex signature)
695694 set *challenges = if sigValid
696695 then case matched of
697696 Just {value: _} -> do
@@ -780,9 +779,8 @@ serve = \req -> case req of
780779 yield (respond {events: result.events, cursor: result.cursor, authorized: result.authorized})
781780
782781 SendMessage {to, encryptedBlob, timestamp, authorization, respond} -> do
783− let validInput = not (to == "") && not (encryptedBlob == "") && not (tooLong to maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && isValidHex to && isValidHex encryptedBlob
784− let to = if isValidHex to then normalizeHex to else to
785− let encryptedBlob = if isValidHex encryptedBlob then normalizeHex encryptedBlob else encryptedBlob
782+ let to = normalizeHex to
783+ let encryptedBlob = normalizeHex encryptedBlob
786784 t <- now
787785 msgId <- genToken
788786 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t - messageMaxAge else timestamp
@@ -801,11 +799,11 @@ serve = \req -> case req of
801799 yield m)
802800 let shouldInsert = case session of
803801 Nothing {} -> False {}
804− Just {value: _} -> validInput && recipientMsgCount < maxMessagesPerRecipient
802+ Just {value: _} -> recipientMsgCount < maxMessagesPerRecipient
805803 set *seqCounter = if shouldInsert then seqTs else seqCounter
806804 set *messages = case session of
807805 Nothing {} -> messages
808− Just {value: s} -> if validInput && recipientMsgCount < maxMessagesPerRecipient
806+ Just {value: s} -> if recipientMsgCount < maxMessagesPerRecipient
809807 then union messages
810808 [{messageId: msgId, toKey: to, fromKey: s.pubkey,
811809 encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: "", receivedAt: seqTs}]
@@ -831,8 +829,7 @@ serve = \req -> case req of
831829 Just {value: s} -> s.pubkey
832830 let status = case session of
833831 Nothing {} -> "unauthorized"
834− _ -> if not validInput then "invalid"
835− else if recipientMsgCount >= maxMessagesPerRecipient then "rejected"
832+ _ -> if recipientMsgCount >= maxMessagesPerRecipient then "rejected"
836833 else if localOnline then "delivered"
837834 else if count remoteServers > 0 then "federated"
838835 else "queued"
@@ -848,7 +845,7 @@ serve = \req -> case req of
848845 yield {status: status, remoteServers: remoteServers, fromKey: fromKey, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, forwardsAtCapacity: forwardsAtCapacity}
849846 let finalStatus = if result.status == "federated" && (federationSecret == "" || result.forwardsAtCapacity) then "queued" else result.status
850847 fork (do
851− if count result.remoteServers > 0 && not (result.status == "unauthorized") && not (result.status == "invalid") && not (result.status == "rejected") && not (federationSecret == "")
848+ if count result.remoteServers > 0 && not (result.status == "unauthorized") && not (result.status == "rejected") && not (federationSecret == "")
852849 then sendToEach result.remoteServers (\server -> do
853850 fwdResult <- fetch (federationScheme ++ server) (ForwardMessage {
854851 toKey: to, fromKey: result.fromKey, encryptedBlob: encryptedBlob,
@@ -864,21 +861,19 @@ serve = \req -> case req of
864861 Err _ -> yield {}
865862 )
866863 else yield {}
867− if result.forwardsAtCapacity && count result.remoteServers > 0 && not (result.status == "unauthorized") && not (result.status == "invalid") && not (result.status == "rejected")
864+ if result.forwardsAtCapacity && count result.remoteServers > 0 && not (result.status == "unauthorized") && not (result.status == "rejected")
868865 then println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")
869866 else yield {}
870867 broadcastOfflineGossip t result.peerServers result.expiredLocal)
871− yield (respond {status: finalStatus, messageId: (if result.status == "unauthorized" then "" else if result.status == "invalid" then "" else if result.status == "rejected" then "" else msgId)})
868+ yield (respond {status: finalStatus, messageId: (if result.status == "unauthorized" then "" else if result.status == "rejected" then "" else msgId)})
872869
873870 AckMessages {messageIds, authorization, respond} -> do
874871 t <- now
875872 let dedupedIds = fold (\acc mid ->
876− if mid == "" then acc
877− else if count acc >= maxMessageIds then acc
878− else if tooLong mid maxMessageIdLen then acc
879− else if not (isValidHex mid) then acc
880− else if count (do a <- acc; where a == normalizeHex mid; yield a) > 0 then acc
881− else union acc [normalizeHex mid]) [] messageIds
873+ let nmid = normalizeHex mid in
874+ if count acc >= maxMessageIds then acc
875+ else if count (do a <- acc; where a == nmid; yield a) > 0 then acc
876+ else union acc [nmid]) [] messageIds
882877 result <- atomic do
883878 pr <- maybePrune t
884879 sessions <- *sessions
@@ -966,8 +961,7 @@ serve = \req -> case req of
966961 yield (respond {ok: result.ok})
967962
968963 LookupPresence {pubkey, authorization, respond} -> do
969− let validPubkey = not (pubkey == "") && not (tooLong pubkey maxPubkeyLen) && isValidHex pubkey
970− let pubkey = if isValidHex pubkey then normalizeHex pubkey else pubkey
964+ let pubkey = normalizeHex pubkey
971965 t <- now
972966 result <- atomic do
973967 pr <- maybePrune t
@@ -977,7 +971,7 @@ serve = \req -> case req of
977971 let authorized = case session of
978972 Nothing {} -> False {}
979973 _ -> True {}
980− let valid = if not validPubkey then [] else do
974+ let valid = do
981975 p <- presence
982976 where p.pubkey == pubkey
983977 where p.expiresAt > t
@@ -985,7 +979,7 @@ serve = \req -> case req of
985979 let foundServer = case session of
986980 Nothing {} -> ""
987981 Just {value: _} ->
988− if not validPubkey || count valid == 0
982+ if count valid == 0
989983 then ""
990984 else if count (do p <- valid; where p.server == serverHostN; yield p) > 0
991985 then serverHostN
@@ -998,8 +992,8 @@ serve = \req -> case req of
998992 t <- now
999993 let authorized = verifyFederationAuth authorization
1000994 let cappedHops = if hopsRemaining > 5 then 5 else hopsRemaining
1001− let fromServer = if tooLong fromServer maxServerNameLen then fromServer else normalizeServerName fromServer
1002− let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && not (fromServer == serverHostN) && not (tooLong fromServer maxServerNameLen) && not (isBadServerName fromServer) && count events <= maxGossipEvents
995+ let fromServer = normalizeServerName fromServer
996+ let validRequest = authorized && cappedHops >= 1 && not (fromServer == serverHostN) && count events <= maxGossipEvents
1003997 let onlineEntries = do
1004998 e <- events
1005999 where validRequest
@@ -1245,42 +1239,41 @@ serve = \req -> case req of
12451239 yield (respond {ok: validRequest})
12461240
12471241 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
1248− let validFields = not (toKey == "") && not (fromKey == "") && not (encryptedBlob == "") && not (messageId == "") && not (tooLong toKey maxPubkeyLen) && not (tooLong fromKey maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && not (tooLong messageId maxMessageIdLen) && isValidHex toKey && isValidHex fromKey && isValidHex encryptedBlob && isValidHex messageId
1249− let toKey = if isValidHex toKey then normalizeHex toKey else toKey
1250− let fromKey = if isValidHex fromKey then normalizeHex fromKey else fromKey
1251− let encryptedBlob = if isValidHex encryptedBlob then normalizeHex encryptedBlob else encryptedBlob
1252− let messageId = if isValidHex messageId then normalizeHex messageId else messageId
1253− let originServer = if tooLong originServer maxServerNameLen then originServer else normalizeServerName originServer
1242+ let toKey = normalizeHex toKey
1243+ let fromKey = normalizeHex fromKey
1244+ let encryptedBlob = normalizeHex encryptedBlob
1245+ let messageId = normalizeHex messageId
1246+ let originServer = normalizeServerName originServer
12541247 t <- now
12551248 let authorized = verifyFederationAuth authorization
1256− let validOrigin = not (originServer == "") && not (originServer == serverHostN) && not (tooLong originServer maxServerNameLen) && not (isBadServerName originServer)
1249+ let validOrigin = not (originServer == serverHostN)
12571250 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t - messageMaxAge else timestamp
12581251 result <- atomic do
12591252 pr <- maybePrune t
12601253 messages <- *messages
12611254 seqCounter <- *seqCounter
12621255 let seqTs = if t > seqCounter then t else seqCounter + 1
1263− let recipientMsgCount = if not (authorized && validOrigin && validFields) then 0
1256+ let recipientMsgCount = if not (authorized && validOrigin) then 0
12641257 else count (do
12651258 m <- messages
12661259 where m.toKey == toKey
12671260 where m.receivedAt >= t - messageMaxAge
12681261 yield m)
1269− let existsInMessages = if not (authorized && validOrigin && validFields) then False {}
1262+ let existsInMessages = if not (authorized && validOrigin) then False {}
12701263 else count (do
12711264 m <- messages
12721265 where m.messageId == messageId
12731266 where m.toKey == toKey
12741267 yield m) > 0
12751268 ackedIds <- *ackedIds
1276− let existsInAcked = if not (authorized && validOrigin && validFields) then False {}
1269+ let existsInAcked = if not (authorized && validOrigin) then False {}
12771270 else count (do
12781271 a <- ackedIds
12791272 where a.messageId == messageId
12801273 where a.toKey == toKey
12811274 yield a) > 0
12821275 let exists = existsInMessages || existsInAcked
1283− let shouldInsert = authorized && validOrigin && validFields && not exists && recipientMsgCount < maxMessagesPerRecipient
1276+ let shouldInsert = authorized && validOrigin && not exists && recipientMsgCount < maxMessagesPerRecipient
12841277 set *seqCounter = if shouldInsert then seqTs else seqCounter
12851278 set *messages = if shouldInsert
12861279 then union messages
@@ -1289,10 +1282,10 @@ serve = \req -> case req of
12891282 else messages
12901283 yield {peerServers: pr.peerServers, expiredLocal: pr.expiredLocal, inserted: shouldInsert, alreadyExists: exists}
12911284 fork (broadcastOfflineGossip t result.peerServers result.expiredLocal)
1292− yield (respond {ok: result.inserted || (authorized && validOrigin && validFields && result.alreadyExists)})
1285+ yield (respond {ok: result.inserted || (authorized && validOrigin && result.alreadyExists)})
12931286
12941287 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
1295− let fromServer = if tooLong fromServer maxServerNameLen then fromServer else normalizeServerName fromServer
1288+ let fromServer = normalizeServerName fromServer
12961289 let originServer = if tooLong originServer maxServerNameLen then originServer else normalizeServerName originServer
12971290 t <- now
12981291 let authorized = verifyFederationAuth authorization
@@ -1303,7 +1296,7 @@ serve = \req -> case req of
13031296 else if not (isValidHex mid) then acc
13041297 else if count (do a <- acc; where a == normalizeHex mid; yield a) > 0 then acc
13051298 else union acc [normalizeHex mid]) [] messageIds
1306− let validAckFields = authorized && originServer == serverHostN && not (fromServer == "") && not (fromServer == serverHostN) && not (tooLong fromServer maxServerNameLen) && not (isBadServerName fromServer)
1299+ let validAckFields = authorized && originServer == serverHostN && not (fromServer == serverHostN)
13071300 pruneResult <- atomic do
13081301 pr <- maybePrune t
13091302 forwards <- *forwards