@@ -82,21 +82,6 @@ maxSessionsPerKey = 10
8282-- Validity window for an auth challenge before it must be re-issued (1 min).
8383challengeExpiry : Int<Ms>
8484challengeExpiry = 60000
85−-- Challenge requests permitted per pubkey inside its rate window.
86−-- Wired into the /auth/challenge route's `rateLimit` clause.
87−maxChallengeRatePerKey : Int
88−maxChallengeRatePerKey = 5
89−-- Window over which the per-pubkey challenge rate is measured (1 s).
90−challengeRateWindowMs : Int<Ms>
91−challengeRateWindowMs = 1000
92−-- Send-message requests permitted per session token per window.
93−-- Wired into the /messages route's `rateLimit` clause.
94−maxSendRatePerKey : Int
95−maxSendRatePerKey = 20
96−-- Window for the per-session send rate limit (1 s).
97−sendRateWindowMs : Int<Ms>
98−sendRateWindowMs = 1000
99−
10085-- How long a session token remains valid after issue (1 hour).
10186sessionExpiry : Int<Ms>
10287sessionExpiry = 3600000
@@ -160,11 +145,7 @@ impl ToJSON GossipEventType where
160145 GossipUnknown {} -> toJson {eventType: "unknown"}
161146
162147impl FromJSON GossipEventType where
163− parseJson t =
164− let raw = parseJson t : {eventType: Text, pubkey: PubkeyHex}
165− if raw.eventType == "online" then GossipOnline {raw.pubkey}
166− else if raw.eventType == "offline" then GossipOffline {raw.pubkey}
167− else GossipUnknown {}
148+ parseJson t = (\raw -> if raw.eventType == "online" then GossipOnline {raw.pubkey} else if raw.eventType == "offline" then GossipOffline {raw.pubkey} else GossipUnknown {}) (parseJson t : {eventType: Text, pubkey: PubkeyHex})
168149
169150-- /messages is all-or-nothing per call: either every message in the batch is
170151-- enqueued (delivered, federated, or buffered for federation), or none are
@@ -260,30 +241,13 @@ type ForwardFailure = {server: Text, failedAt: Timestamp, failures: Int}
260241-- declared body shape, `Err` produces a non-2xx response whose body is
261242-- `{"error": "<code>"}`.
262243
263−-- Per-pubkey token bucket on /auth/challenge: prevents one key from
264−-- exhausting the challenge pool by repeated nonce requests.
265−challengeRateLimit = {
266− key: \input ctx -> Just {value: input.pubkey},
267− limit: {requests: maxChallengeRatePerKey, window: challengeRateWindowMs}
268−}
269−
270−-- Per-session token bucket on /messages, keyed by the Bearer token. A pubkey
271−-- is capped at `maxSessionsPerKey` live sessions, so the effective per-pubkey
272−-- send budget is bounded as well.
273−sendRateLimit = {
274− key: \input ctx -> Just {value: input.authorization},
275− limit: {requests: maxSendRatePerKey, window: sendRateWindowMs}
276−}
277−
278244route AuthApi where
279− POST {pubkey: PubkeyHex} /auth/challenge headers {xForwardedFor: Maybe Text} -> {challenge: Text, expiresAt: Timestamp}
280− rateLimit challengeRateLimit = ReqChallenge
245+ POST {pubkey: PubkeyHex} /auth/challenge headers {xForwardedFor: Maybe Text} -> {challenge: Text, expiresAt: Timestamp} = ReqChallenge
281246 POST {pubkey: PubkeyHex, challenge: Text, signature: SignatureHex, revokeOthers: Bool} /auth/verify headers {xForwardedFor: Maybe Text} -> {token: Text, expiresAt: Timestamp} = VerifyAuth
282247
283248route ClientApi where
284249 POST {cursor: Timestamp} /poll headers {authorization: Text, xForwardedFor: Maybe Text} -> {events: [PollEvent], cursor: Timestamp} = Poll
285− POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, xForwardedFor: Maybe Text} -> {}
286− rateLimit sendRateLimit = SendMessages
250+ POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, xForwardedFor: Maybe Text} -> {} = SendMessages
287251
288252route FederationApi where
289253 POST {events: [GossipEventType], fromServer: ServerName} /federation/gossip -> {} = RecvGossip
@@ -471,7 +435,9 @@ forwardMatches = \blob server f -> f.encryptedBlob == blob && f.toServer == serv
471435refreshLocalPresence : PubkeyHex -> Timestamp -> IO {rw *presence} [Text]
472436refreshLocalPresence = \pubkey expiry -> do
473437 pres <- *presence
474− *presence = union (filter (\p -> not (p.pubkey == pubkey && isLocalServer p.server)) pres) [{pubkey, server: serverHost, expiresAt: expiry}]
438+ *presence = union (filter (\p -> not (p.pubkey == pubkey && isLocalServer p.server)) pres) [
439+ {pubkey, server: serverHost, expiresAt: expiry}
440+ ]
475441 yield (collectPeers pres (\p -> p.server))
476442
477443-- ============================================
@@ -654,9 +620,7 @@ handlePoll = \cursor authorization clientIp -> do
654620 startWakeup <- *pollWakeup
655621 pres <- *presence
656622 let stale = not any (\p -> p.pubkey == pubkey && isLocalServer p.server && p.expiresAt - t >= sessionExpiry / 2) pres
657− refreshPeers <- if stale
658− then refreshLocalPresence pubkey (t + sessionExpiry)
659− else yield []
623+ refreshPeers <- if stale then refreshLocalPresence pubkey (t + sessionExpiry) else yield []
660624 yield (Just {value: {pubkey, clampedCursor, startWakeup, refreshPeers}}))
661625 case setup of
662626 Nothing {} -> yield (Err {error: {status: 401, message: "unauthorized"}})
@@ -682,7 +646,10 @@ doPoll = \s t -> do
682646 msgs <- *messages
683647 let fresh = filter (\m -> m.toKey == s.pubkey && tnow - m.receivedAt < messageMaxAge && m.receivedAt > s.clampedCursor) msgs
684648 let limited = take maxPollEvents (sortBy (\m -> m.receivedAt) fresh)
685− yield {events: map (\m -> {m.encryptedBlob}) limited, cursor: fold (\acc m -> max acc m.receivedAt) s.clampedCursor limited})
649+ yield {
650+ events: map (\m -> {m.encryptedBlob}) limited,
651+ cursor: fold (\acc m -> max acc m.receivedAt) s.clampedCursor limited
652+ })
686653 initial <- collectAt t
687654 if count initial.events > 0
688655 then yield initial
@@ -740,7 +707,7 @@ handleSendMessages = \messages authorization clientIp -> do
740707 appendMessage c.to c.encryptedBlob t
741708 when federationEnabled (do
742709 fwds <- *forwards
743− let newFwds = map (\s -> {toServer: s, toKey: c.to, encryptedBlob: c.encryptedBlob, retries: 0}) (filter (\s -> not any (forwardMatches c.encryptedBlob s) fwds) c.remoteServers)
710+ let newFwds = map (\s -> {toServer: s, toKey: c.to, c.encryptedBlob, retries: 0}) (filter (\s -> not any (forwardMatches c.encryptedBlob s) fwds) c.remoteServers)
744711 *forwards = union fwds newFwds))
745712 yield staged)
746713 when federationEnabled (forEach committed (\c -> fork (forkForwardsTo c.to c.encryptedBlob c.remoteServers)))
@@ -796,7 +763,10 @@ handleRecvGossip = \events fromServer -> do
796763 newlyOnline <- atomic (do
797764 pres <- *presence
798765 let combined = fold (\acc e -> case e of
799− GossipOnline {pubkey} -> {touchedPubkeys: union acc.touchedPubkeys [pubkey], added: union acc.added [{pubkey, server: fromServer, expiresAt: t + onlineGossipTtl}]}
766+ GossipOnline {pubkey} -> {
767+ touchedPubkeys: union acc.touchedPubkeys [pubkey],
768+ added: union acc.added [{pubkey, server: fromServer, expiresAt: t + onlineGossipTtl}]
769+ }
800770 GossipOffline {pubkey} -> {touchedPubkeys: union acc.touchedPubkeys [pubkey], acc.added}
801771 _ -> acc) {touchedPubkeys: [], added: []} events
802772 let kept = filter (\p -> p.server != fromServer || not elem p.pubkey combined.touchedPubkeys) pres
@@ -816,7 +786,7 @@ handleForwardMessage = \toKey encryptedBlob -> do
816786 t <- now
817787 atomic (do
818788 pres <- *presence
819− if not (hasLocalPresence pres toKey t)
789+ if not hasLocalPresence pres toKey t
820790 then yield (Err {error: {status: 404, message: "no_presence"}})
821791 else do
822792 appendMessage toKey encryptedBlob t