@@ -46,7 +46,9 @@ federationScheme = "https://"
4646-- The Ed25519 federation keypair lives in `*federationPrivateKeyHex` /
4747-- `*federationPublicKeyHex` refs and is generated on first startup; see `main`.
4848
49−-- Drop stored messages older than this (30 days).
49+-- Storage retention for messages (and clamp on client-supplied wall-clock
50+-- timestamps + dedup-id retention). Stored messages live this long; sends
51+-- whose `timestamp` is older than this are rejected (30 days).
5052messageMaxAge : Int<Ms>
5153messageMaxAge = 2592000000
5254-- Upper bound on TTL a client may request for a presence entry (1 hour).
@@ -203,12 +205,6 @@ maxPollsPerKey = 3
203205presenceRefreshInterval : Int<Ms>
204206presenceRefreshInterval = 300000
205207
206−-- Drop unacked messages older than this (7 days).
207−unackedMessageMaxAge : Int<Ms>
208−unackedMessageMaxAge = 604800000
209−-- Floor on the TTL a client may request when sending a message (1 min).
210−minMessageTtl : Int<Ms>
211−minMessageTtl = 60000
212208-- Window during which a delivered-but-unacked message is retained for re-delivery (2 min).
213209ackGracePeriod : Int<Ms>
214210ackGracePeriod = 120000
@@ -246,7 +242,7 @@ sToMs = \s -> stripUnit s * 1000
246242-- delivery metadata for the recipient; the server cannot read the blob.
247243type PollEvent = {
248244 id: MessageIdHex,
249− payload: {encryptedBlob: BlobHex, timestamp: Timestamp, toKey: PubkeyHex, expiresAt: Timestamp}
245+ payload: {encryptedBlob: BlobHex, timestamp: Timestamp, toKey: PubkeyHex}
250246}
251247
252248-- Kind of presence event flowing through federation gossip. `GossipUnknown` is
@@ -340,13 +336,13 @@ type Session = {
340336}
341337-- Mailbox row: a stored message awaiting recipient pickup or federation forward.
342338-- Identity is `messageIdOf encryptedBlob` (BLAKE3 of the ciphertext); we don't
343−-- persist it because it's deterministically derivable.
339+-- persist it because it's deterministically derivable. Storage TTL is fixed
340+-- at `messageMaxAge`, so expiry is derived from `receivedAt`.
344341type Message = {
345342 toKey: PubkeyHex,
346343 encryptedBlob: BlobHex,
347344 timestamp: Timestamp,
348− receivedAt: Timestamp,
349− expiresAt: Timestamp
345+ receivedAt: Timestamp
350346}
351347-- Last-known location of a pubkey, either local (`server == serverHostN`) or
352348-- learned from a peer via gossip. Multiple entries per pubkey are allowed.
@@ -365,8 +361,7 @@ type ForwardEntry = {
365361 toKey: PubkeyHex,
366362 encryptedBlob: BlobHex,
367363 timestamp: Timestamp,
368− retries: Int,
369− expiresAt: Timestamp
364+ retries: Int
370365}
371366-- Replay-protection record: a message id we already delivered locally.
372367type DeliveredEntry = {messageId: MessageIdHex, toKey: PubkeyHex, deliveredAt: Timestamp}
@@ -379,7 +374,7 @@ type RateEntry = {key: Text, windowStart: Timestamp, count: Int}
379374type ActivePoll = {pubkey: PubkeyHex, startedAt: Timestamp}
380375
381376-- One entry of a /messages/batch request body.
382−type BatchMessage = {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, expiresInMs: Int}
377+type BatchMessage = {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}
383378-- Per-entry result returned by /messages/batch.
384379type BatchResult = {messageId: MessageIdHex, status: MessageStatus}
385380
@@ -457,7 +452,7 @@ route AuthApi where
457452
458453route ClientApi where
459454 POST {cursor: Timestamp, generation: Int} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp, authorized: Bool, rateLimited: Bool, draining: Bool, generation: Int, heartbeat: Bool, retryAfterMs: Int, cursorReset: Bool, hasMore: Bool, cursorStale: Bool, loadPct: Int} = Poll
460− POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, expiresInMs: Int} /messages headers {authorization: Text, clientIp: Text} -> {status: MessageStatus, messageId: MessageIdHex, retryAfterMs: Int, error: Text, loadPct: Int} = SendMessage
455+ POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp} /messages headers {authorization: Text, clientIp: Text} -> {status: MessageStatus, messageId: MessageIdHex, retryAfterMs: Int, error: Text, loadPct: Int} = SendMessage
461456 POST {to: PubkeyHex, messageId: MessageIdHex} /messages/status headers {authorization: Text, clientIp: Text} -> {status: Text, authorized: Bool} = MessageStatus
462457 POST {messageIds: [MessageIdHex]} /messages/ack headers {authorization: Text, clientIp: Text} -> {ok: Bool, authorized: Bool, ackedCount: Int, error: Text} = AckMessages
463458 POST {messages: [BatchMessage]} /messages/batch headers {authorization: Text, clientIp: Text} -> {results: [BatchResult], authorized: Bool, retryAfterMs: Int} = SendBatch
@@ -468,8 +463,8 @@ route ClientApi where
468463
469464route FederationApi where
470465 POST {events: [GossipEvent], hopsRemaining: Int, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/gossip headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = RecvGossip
471− POST {toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, messageId: MessageIdHex, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text, expiresAt: Timestamp} /federation/forward headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
472− POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, messageId: MessageIdHex, expiresAt: Timestamp}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} = FederationBatchForward
466+ POST {toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, messageId: MessageIdHex, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
467+ POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, messageId: MessageIdHex}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} = FederationBatchForward
473468
474469route FederationPubkeyApi where
475470 GET /federation/pubkey -> {pubkey: Text, ok: Bool} = FederationPubkey
@@ -602,7 +597,7 @@ pct : Int -> Int -> Int
602597pct = \cur maxN -> if maxN > 0 then cur * 100 / maxN else 0
603598
604599-- Common ack/delete tail: pull matching messages, drop them and their forwards.
605−removeForKey : (forall r. {toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, expiresAt: Timestamp | r} -> Bool) -> IO {rw *forwards, rw *messages} [Message]
600+removeForKey : (forall r. {toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp | r} -> Bool) -> IO {rw *forwards, rw *messages} [Message]
606601removeForKey = \pred -> do
607602 msgs <- *messages
608603 fwds <- *forwards
@@ -1016,13 +1011,6 @@ clampTtl = clamp 1 maxPresenceTtl
10161011clampTimestamp : Timestamp -> Timestamp -> Timestamp
10171012clampTimestamp = \timestamp t -> clamp (t - messageMaxAge) t timestamp
10181013
1019−-- Resolve the effective TTL for a stored message: 0/negative means default,
1020−-- otherwise clamp the requested value into [minMessageTtl, unackedMessageMaxAge].
1021−effectiveMessageTtl : Int<Ms> -> Int<Ms>
1022−effectiveMessageTtl = \e -> if e <= 0
1023−then unackedMessageMaxAge
1024−else clamp minMessageTtl unackedMessageMaxAge e
1025−
10261014-- Strictly-monotonic next sequence timestamp: max of wall clock and prior+1.
10271015-- Guarantees /poll cursors order events even when many arrive in the same ms.
10281016nextSeqTs : Timestamp -> Timestamp -> Timestamp
@@ -1048,9 +1036,7 @@ maskedServer = \pres pubkey t -> (\here -> if any here (\p -> isLocalServer p.se
10481036-- The blob comparison avoids hashing every live row on each check; the
10491037-- tombstone log only stores the id, so we compare it directly.
10501038isDuplicateMessage : BlobHex -> PubkeyHex -> [Message] -> [DeliveredEntry] -> Bool
1051−isDuplicateMessage = \blob toKey msgs dids ->
1052− any msgs (\m -> m.toKey == toKey && m.encryptedBlob == blob) ||
1053− (\mid -> any dids (\d -> d.toKey == toKey && d.messageId == mid)) (messageIdOf blob)
1039+isDuplicateMessage = \blob toKey msgs dids -> any msgs (\m -> m.toKey == toKey && m.encryptedBlob == blob) || (\mid -> any dids (\d -> d.toKey == toKey && d.messageId == mid)) (messageIdOf blob)
10541040
10551041-- Project items through `getServer`, drop our own hostname, and dedupe.
10561042-- Used to derive peer recipient lists from forwards/presence/etc.
@@ -1083,9 +1069,9 @@ pruneExpired = \t -> atomic do
10831069 ses <- *sessions
10841070 *sessions = filter ses (\x -> x.expiresAt > t)
10851071 msgs <- *messages
1086− *messages = filter msgs (\m -> m.expiresAt > t)
1072+ *messages = filter msgs (\m -> t - m.receivedAt < messageMaxAge)
10871073 fwds <- *forwards
1088− *forwards = filter fwds (\f -> f.expiresAt > t)
1074+ *forwards = filter fwds (\f -> t - f.createdAt < messageMaxAge)
10891075 dids <- *deliveredIds
10901076 *deliveredIds = filter dids (\d -> t - d.deliveredAt < messageMaxAge)
10911077 ff <- *forwardFailures
@@ -1174,8 +1160,7 @@ retryPendingForwards = do
11741160 fromServer: serverHostN,
11751161 requestTimestamp: t2,
11761162 authorization: auth,
1177− correlationId: "",
1178− f.expiresAt
1163+ correlationId: ""
11791164 })
11801165 success <- case result of
11811166 Err _ -> do
@@ -1189,15 +1174,7 @@ retryPendingForwards = do
11891174 atomic (do
11901175 cur <- *forwards
11911176 *forwards = pluck (removeWhere cur isDone) (\x -> if isMatch x
1192− then {
1193− x.toServer,
1194− x.createdAt,
1195− x.toKey,
1196− x.encryptedBlob,
1197− x.timestamp,
1198− retries: x.retries + 1,
1199− x.expiresAt
1200− }
1177+ then {x.toServer, x.createdAt, x.toKey, x.encryptedBlob, x.timestamp, retries: x.retries + 1}
12011178 else x))))
12021179
12031180-- Forward queued messages to a server when a remote key comes online.
@@ -1212,8 +1189,7 @@ forwardQueuedToServer = \pubkey server t -> atomic (do
12121189 m.toKey,
12131190 m.encryptedBlob,
12141191 m.timestamp,
1215− retries: 0,
1216− m.expiresAt
1192+ retries: 0
12171193 })
12181194 *forwards = union fwds newFwds)
12191195
@@ -1470,7 +1446,7 @@ pollOnce = \authorization cursor cursorWasReset clientIp serverGen respond t ->
14701446
14711447-- One pass: collect events at `t`. If empty, retry-wait on `pollWakeup`
14721448-- (bumped by send/heartbeat) and re-collect once at the new wall time.
1473−doPoll : {clampedCursor: Int, pubkey: PubkeyHex, startWakeup: Timestamp} -> Int<Ms> -> IO {r *pollWakeup, clock} {cursor: Int<Ms>, events: [{id: MessageIdHex, payload: {encryptedBlob: BlobHex, expiresAt: Timestamp, timestamp: Timestamp, toKey: PubkeyHex}}], hasMore: Bool}
1449+doPoll : {clampedCursor: Int, pubkey: PubkeyHex, startWakeup: Timestamp} -> Int<Ms> -> IO {r *pollWakeup, clock} {cursor: Int<Ms>, events: [{id: MessageIdHex, payload: {encryptedBlob: BlobHex, timestamp: Timestamp, toKey: PubkeyHex}}], hasMore: Bool}
14741450doPoll = \s t -> do
14751451 let collectAt = \tnow -> atomic (do
14761452 msgs <- *messages
@@ -1486,7 +1462,7 @@ doPoll = \s t -> do
14861462-- Pull this pubkey's unexpired messages newer than `cursor`, sort by
14871463-- receivedAt, take up to `maxPollEvents`, and report `hasMore` if truncated.
14881464collectPollEvents : [Message] -> PubkeyHex -> Timestamp -> Timestamp -> {events: [PollEvent], cursor: Timestamp, hasMore: Bool}
1489−collectPollEvents = \messages pubkey cursor t -> (\myMsgs -> (\limited -> {events: pluck limited (\m -> {id: messageIdOf m.encryptedBlob, payload: {m.toKey, m.encryptedBlob, m.timestamp, m.expiresAt}}), cursor: fold (\acc m -> max acc m.receivedAt) cursor limited, hasMore: count myMsgs > count limited}) (takeRelation maxPollEvents (sortBy (\m -> m.receivedAt) myMsgs))) (filter messages (\m -> m.toKey == pubkey && m.expiresAt > t && m.receivedAt > cursor))
1465+collectPollEvents = \messages pubkey cursor t -> (\myMsgs -> (\limited -> {events: pluck limited (\m -> {id: messageIdOf m.encryptedBlob, payload: {m.toKey, m.encryptedBlob, m.timestamp}}), cursor: fold (\acc m -> max acc m.receivedAt) cursor limited, hasMore: count myMsgs > count limited}) (takeRelation maxPollEvents (sortBy (\m -> m.receivedAt) myMsgs))) (filter messages (\m -> m.toKey == pubkey && t - m.receivedAt < messageMaxAge && m.receivedAt > cursor))
14901466
14911467-- Treat the supplied cursor as an ack: drop all messages with
14921468-- receivedAt <= cursor, plus their pending forwards.
@@ -1515,8 +1491,8 @@ ackPolledMessages = \pubkey cursor -> do
15151491-- POST /messages: send one ciphertext to `to`. Rejects on draining/rate
15161492-- limit/stale timestamp; otherwise calls `sendMessageCore` inside an atomic
15171493-- block and forks the federation forward (if any) once the local commit lands.
1518−handleSendMessage : PubkeyHex -> BlobHex -> Int<Ms> -> Int -> Text -> Text -> ({error: Text, loadPct: Int, messageId: MessageIdHex, retryAfterMs: Int, status: MessageStatus} -> a) -> IO {r *drainPhase, rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock, random} a
1519−handleSendMessage = \to encryptedBlob timestamp expiresInMs authorization clientIp respond -> do
1494+handleSendMessage : PubkeyHex -> BlobHex -> Int<Ms> -> Text -> Text -> ({error: Text, loadPct: Int, messageId: MessageIdHex, retryAfterMs: Int, status: MessageStatus} -> a) -> IO {r *deliveredIds, r *drainPhase, r *presence, r *sessions, rw *forwards, rw *globalRateCount, rw *globalRateWindowStart, rw *messages, rw *pairRates, rw *pollWakeup, rw *recvRates, rw *sendRates, rw *seqCounter, clock} a
1495+handleSendMessage = \to encryptedBlob timestamp authorization clientIp respond -> do
15201496 t <- now
15211497 dp <- *drainPhase
15221498 g <- checkGlobalRate t
@@ -1531,7 +1507,7 @@ handleSendMessage = \to encryptedBlob timestamp expiresInMs authorization client
15311507 else do
15321508 let clampedTs = clampTimestamp timestamp t
15331509 result <- atomic (do
1534− withSessionAuth authorization t clientIp (yield {status: MsUnauthorized {}, remoteServers: []}) (\senderKey -> sendMessageCore senderKey to encryptedBlob clampedTs expiresInMs t))
1510+ withSessionAuth authorization t clientIp (yield {status: MsUnauthorized {}, remoteServers: []}) (\senderKey -> sendMessageCore senderKey to encryptedBlob clampedTs t))
15351511 when federationEnabled (fork (forkForwardsTo to encryptedBlob clampedTs result.remoteServers))
15361512 let ok = isSendSuccess result.status
15371513 yield (respond {
@@ -1548,8 +1524,8 @@ handleSendMessage = \to encryptedBlob timestamp expiresInMs authorization client
15481524-- `seqCounter`, appends the message, and pre-stages forwards for any remote
15491525-- presence rows that already exist for `to`. Returns the chosen status and
15501526-- the list of remote servers the caller should kick forwards to.
1551−sendMessageCore : PubkeyHex -> PubkeyHex -> BlobHex -> Int<Ms> -> Int -> Int<Ms> -> IO {r *deliveredIds, rw *forwards, rw *messages, rw *pairRates, rw *pollWakeup, r *presence, rw *recvRates, rw *sendRates, rw *seqCounter} {remoteServers: [Text], status: MessageStatus}
1552−sendMessageCore = \senderKey to encryptedBlob clampedTs expiresInMs t -> do
1527+sendMessageCore : PubkeyHex -> PubkeyHex -> BlobHex -> Int<Ms> -> Int<Ms> -> IO {r *deliveredIds, rw *forwards, rw *messages, rw *pairRates, rw *pollWakeup, r *presence, rw *recvRates, rw *sendRates, rw *seqCounter} {remoteServers: [Text], status: MessageStatus}
1528+sendMessageCore = \senderKey to encryptedBlob clampedTs t -> do
15531529 msgs <- *messages
15541530 dids <- *deliveredIds
15551531 sRates <- *sendRates
@@ -1576,16 +1552,7 @@ sendMessageCore = \senderKey to encryptedBlob clampedTs expiresInMs t -> do
15761552 seq <- *seqCounter
15771553 let seqTs = nextSeqTs t seq
15781554 *seqCounter = seqTs
1579− let expiresAt = seqTs + effectiveMessageTtl expiresInMs
1580− *messages = union msgs [
1581− {
1582− toKey: to,
1583− encryptedBlob,
1584− timestamp: clampedTs,
1585− receivedAt: seqTs,
1586− expiresAt
1587− }
1588− ]
1555+ *messages = union msgs [{toKey: to, encryptedBlob, timestamp: clampedTs, receivedAt: seqTs}]
15891556 *pollWakeup = seqTs
15901557 let toPresence = presenceForKey pres to t
15911558 let remoteServers = collectPeers toPresence (\p -> p.server)
@@ -1598,8 +1565,7 @@ sendMessageCore = \senderKey to encryptedBlob clampedTs expiresInMs t -> do
15981565 toKey: to,
15991566 encryptedBlob,
16001567 timestamp: clampedTs,
1601− retries: 0,
1602− expiresAt
1568+ retries: 0
16031569 })
16041570 *forwards = union fwds newFwds
16051571 yield (count newFwds > 0)
@@ -1635,8 +1601,7 @@ forkForwardsTo = \to encryptedBlob clampedTs servers -> forEach servers (\server
16351601 fromServer: serverHostN,
16361602 requestTimestamp: t,
16371603 authorization: auth,
1638− correlationId: "",
1639− f.expiresAt
1604+ correlationId: ""
16401605 })
16411606 case result of
16421607 Err _ -> recordFailure server
@@ -1757,7 +1722,7 @@ handleListMessages = \offset authorization clientIp respond -> do
17571722 then yield (respond {messages: [], authorized: True {}, count: 0, error: "rate_limited"})
17581723 else do
17591724 msgs <- *messages
1760− let myMsgs = sortBy (\m -> m.receivedAt) (filter msgs (\m -> m.toKey == pubkey && m.expiresAt > t))
1725+ let myMsgs = sortBy (\m -> m.receivedAt) (filter msgs (\m -> m.toKey == pubkey && t - m.receivedAt < messageMaxAge))
17611726 let total = count myMsgs
17621727 let page = pluck (takeRelation maxPollEvents (dropRelation (clamp 0 total offset) myMsgs)) (\m -> {
17631728 messageId: messageIdOf m.encryptedBlob,
@@ -1791,7 +1756,7 @@ handleDeleteMessages = \messageIds authorization clientIp respond -> do
17911756-- POST /messages/batch: multi-recipient send in one call. Each entry runs
17921757-- through `sendMessageCore` separately so per-entry rate limits/errors don't
17931758-- abort the rest; federated forwards are forked per entry.
1794−handleSendBatch : [{encryptedBlob: BlobHex, expiresInMs: Int, timestamp: Int<Ms>, to: PubkeyHex | a}] -> Text -> Text -> ({authorized: Bool, results: [BatchResult], retryAfterMs: Int} -> b) -> IO {r *drainPhase, rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock} b
1759+handleSendBatch : [{encryptedBlob: BlobHex, timestamp: Int<Ms>, to: PubkeyHex | a}] -> Text -> Text -> ({authorized: Bool, results: [BatchResult], retryAfterMs: Int} -> b) -> IO {r *drainPhase, rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock} b
17951760handleSendBatch = \messages authorization clientIp respond -> do
17961761 t <- now
17971762 let mkErr = \_ -> {results: [], authorized: False {}, retryAfterMs: 0}
@@ -1801,8 +1766,7 @@ handleSendBatch = \messages authorization clientIp respond -> do
18011766 let clampedTs = clampTimestamp m.timestamp t
18021767 r <- if t - m.timestamp > messageMaxAge
18031768 then yield {status: MsRejected {}, remoteServers: []}
1804− else atomic (do
1805− sendMessageCore senderKey m.to m.encryptedBlob clampedTs m.expiresInMs t)
1769+ else atomic (do sendMessageCore senderKey m.to m.encryptedBlob clampedTs t)
18061770 when federationEnabled (fork (forkForwardsTo m.to m.encryptedBlob clampedTs r.remoteServers))
18071771 yield {messageId: effMsgId, r.status})
18081772 yield (respond {results, authorized: True {}, retryAfterMs: 0}))
@@ -1870,8 +1834,8 @@ handleRecvGossip = \events hopsRemaining fromServer requestTimestamp authorizati
18701834-- on stale gossip about R could land on B with no actively-polling client and
18711835-- get stuck. If B refuses, A retries via `retryPendingForwards` against the
18721836-- next presence target.
1873−storeFederated : PubkeyHex -> BlobHex -> Int<Ms> -> Int<Ms> -> Bool -> Int<Ms> -> IO {r *deliveredIds, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter} Text
1874−storeFederated = \toKey encryptedBlob timestamp expiresAt checkRecvRate t -> do
1837+storeFederated : PubkeyHex -> BlobHex -> Int<Ms> -> Bool -> Int<Ms> -> IO {r *deliveredIds, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter} Text
1838+storeFederated = \toKey encryptedBlob timestamp checkRecvRate t -> do
18751839 pres <- *presence
18761840 msgs <- *messages
18771841 dids <- *deliveredIds
@@ -1892,18 +1856,9 @@ storeFederated = \toKey encryptedBlob timestamp expiresAt checkRecvRate t -> do
18921856 else do
18931857 seq <- *seqCounter
18941858 let seqTs = nextSeqTs t seq
1895− let cappedExpiry = if expiresAt > t
1896− then min expiresAt (seqTs + unackedMessageMaxAge)
1897− else seqTs + unackedMessageMaxAge
18981859 *seqCounter = seqTs
18991860 *messages = union msgs [
1900− {
1901− toKey,
1902− encryptedBlob,
1903− timestamp: clampTimestamp timestamp t,
1904− receivedAt: seqTs,
1905− expiresAt: cappedExpiry
1906− }
1861+ {toKey, encryptedBlob, timestamp: clampTimestamp timestamp t, receivedAt: seqTs}
19071862 ]
19081863 *pollWakeup = seqTs
19091864 yield ""
@@ -1915,21 +1870,21 @@ storeFederated = \toKey encryptedBlob timestamp expiresAt checkRecvRate t -> do
19151870-- the blob ourselves so a hostile peer cannot bypass dedup by varying the
19161871-- id. Honest senders derive the same value, so the "forward:" ++ messageId
19171872-- auth scope still verifies.
1918−handleForwardMessage : PubkeyHex -> BlobHex -> Int<Ms> -> Text -> Text -> Int -> Int<Ms> -> Text -> ({error: Text, ok: Bool, retryAfterMs: Int} -> a) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, r *deliveredIds, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter, clock, network} a
1919−handleForwardMessage = \toKey encryptedBlob timestamp _ fromServer requestTimestamp expiresAt authorization respond -> do
1873+handleForwardMessage : PubkeyHex -> BlobHex -> Int<Ms> -> Text -> Text -> Int -> Text -> ({error: Text, ok: Bool, retryAfterMs: Int} -> a) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, r *deliveredIds, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter, clock, network} a
1874+handleForwardMessage = \toKey encryptedBlob timestamp _ fromServer requestTimestamp authorization respond -> do
19201875 let messageId = messageIdOf encryptedBlob
19211876 t <- now
19221877 withFederationAuth authorization t respond fromServer ("forward:" ++ messageId) requestTimestamp (isRemote fromServer) (do
19231878 if t - timestamp > messageMaxAge
19241879 then yield (respond {ok: False {}, error: "stale_timestamp", retryAfterMs: 0})
19251880 else do
1926− result <- atomic storeFederated toKey encryptedBlob timestamp expiresAt (True {}) t
1881+ result <- atomic storeFederated toKey encryptedBlob timestamp (True {}) t
19271882 yield (respond {ok: result == "", error: result, retryAfterMs: 0}))
19281883
19291884-- POST /federation/forward-batch: bulk variant of `handleForwardMessage`.
19301885-- Skips per-entry recv-rate charging (the per-server federation rate limit
19311886-- already gates the call as a whole).
1932−handleFederationBatchForward : [{encryptedBlob: BlobHex, expiresAt: Int<Ms>, messageId: MessageIdHex, timestamp: Int<Ms>, toKey: PubkeyHex | a}] -> Text -> Int -> Text -> ({error: Text, ok: Bool, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} -> b) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, clock, network} b
1887+handleFederationBatchForward : [{encryptedBlob: BlobHex, messageId: MessageIdHex, timestamp: Int<Ms>, toKey: PubkeyHex | a}] -> Text -> Int -> Text -> ({error: Text, ok: Bool, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} -> b) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, clock, network} b
19331888handleFederationBatchForward = \forwards fromServer requestTimestamp authorization respond -> do
19341889 t <- now
19351890 let batchRespond = \r -> respond {r.ok, r.error, results: [], r.retryAfterMs}
@@ -1940,7 +1895,7 @@ handleFederationBatchForward = \forwards fromServer requestTimestamp authorizati
19401895 ok <- if t - e.timestamp > messageMaxAge
19411896 then yield (False {})
19421897 else do
1943− result <- atomic storeFederated e.toKey e.encryptedBlob e.timestamp e.expiresAt (False {}) t
1898+ result <- atomic storeFederated e.toKey e.encryptedBlob e.timestamp (False {}) t
19441899 yield (result == "")
19451900 yield {messageId, ok})
19461901 yield (respond {ok: True {}, error: "", results, retryAfterMs: 0}))
@@ -2640,7 +2595,7 @@ serve = \req -> case req of
26402595 RevokeSession {targetToken, authorization, clientIp, respond} -> handleRevoke targetToken authorization clientIp respond
26412596 Logout {authorization, clientIp, respond} -> handleLogout authorization clientIp respond
26422597 Poll {cursor, generation, authorization, clientIp, respond} -> handlePoll cursor generation authorization clientIp respond
2643− SendMessage {to, encryptedBlob, timestamp, expiresInMs, authorization, clientIp, respond} -> handleSendMessage to encryptedBlob timestamp expiresInMs authorization clientIp respond
2598+ SendMessage {to, encryptedBlob, timestamp, authorization, clientIp, respond} -> handleSendMessage to encryptedBlob timestamp authorization clientIp respond
26442599 MessageStatus {to, messageId, authorization, clientIp, respond} -> handleMessageStatus to messageId authorization clientIp respond
26452600 AckMessages {messageIds, authorization, clientIp, respond} -> handleAckMessages messageIds authorization clientIp respond
26462601 SendBatch {messages, authorization, clientIp, respond} -> handleSendBatch messages authorization clientIp respond
@@ -2649,7 +2604,7 @@ serve = \req -> case req of
26492604 LookupPresence {pubkey, authorization, clientIp, respond} -> handleLookupPresence pubkey authorization clientIp respond
26502605 BatchLookupPresence {pubkeys, authorization, clientIp, respond} -> handleBatchLookupPresence pubkeys authorization clientIp respond
26512606 RecvGossip {events, hopsRemaining, fromServer, requestTimestamp, authorization, respond} -> handleRecvGossip events hopsRemaining fromServer requestTimestamp authorization respond
2652− ForwardMessage {toKey, encryptedBlob, timestamp, messageId, fromServer, requestTimestamp, expiresAt, authorization, respond} -> handleForwardMessage toKey encryptedBlob timestamp messageId fromServer requestTimestamp expiresAt authorization respond
2607+ ForwardMessage {toKey, encryptedBlob, timestamp, messageId, fromServer, requestTimestamp, authorization, respond} -> handleForwardMessage toKey encryptedBlob timestamp messageId fromServer requestTimestamp authorization respond
26532608 FederationBatchForward {forwards, fromServer, requestTimestamp, authorization, respond} -> handleFederationBatchForward forwards fromServer requestTimestamp authorization respond
26542609 FederationPubkey {respond} -> handleFederationPubkey respond
26552610 Prune {authorization, respond} -> handlePrune authorization respond