@@ -77,9 +77,6 @@ maxForwardsPerServer = 2000
7777-- Max accepted length of a server hostname.
7878maxServerNameLen : Int
7979maxServerNameLen = 128
80−-- Max accepted length of a hex-encoded message ID.
81−maxMessageIdLen : Int
82−maxMessageIdLen = 128
8380-- Ceiling on outstanding auth challenges held in memory.
8481maxChallenges : Int
8582maxChallenges = 10000
@@ -204,8 +201,7 @@ requireClientIp = True {}
204201-- applied at federation egress, not in the type predicate.
205202
206203-- One event in a /poll response: the opaque cipher blob addressed to the
207−-- polling pubkey. The server cannot read the blob; clients derive any
208−-- per-message id locally as `messageIdOf encryptedBlob`.
204+-- polling pubkey. The server cannot read the blob.
209205type PollEvent = {encryptedBlob: BlobHex}
210206
211207-- Kind of presence event flowing through federation gossip. `GossipUnknown` is
@@ -244,10 +240,6 @@ type PubkeyHex = Text where isValidExactHex pubkeyLen
244240type BlobHex = Text where isValidBoundedHex maxBlobLen
245241-- Hex-encoded Ed25519 signature (64 bytes → 128 chars exact).
246242type SignatureHex = Text where isValidExactHex signatureLen
247−isValidMessageId : Text -> Bool
248−isValidMessageId = isValidBoundedHex maxMessageIdLen
249−-- Hex-encoded message id, length ≤ `maxMessageIdLen`.
250−type MessageIdHex = Text where isValidMessageId
251243-- Normalized hostname (lowercased, no trailing dot, length-bounded). The SSRF
252244-- deny-list (`isBadServerName`) is enforced separately at federation egress
253245-- (`withFederationAuth`, `handlePingPeer`); keeping it out of the type predicate
@@ -265,11 +257,9 @@ type Challenge = {pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp, ip:
265257-- An issued bearer token bound to (pubkey, ip, expiry).
266258type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp, ip: Text}
267259-- Mailbox row: a stored message awaiting recipient pickup or federation forward.
268−-- Identity is `messageIdOf encryptedBlob` (BLAKE3 of the ciphertext); we don't
269−-- persist it because it's deterministically derivable. Storage TTL is fixed
270−-- at `messageMaxAge`, so expiry is derived from `receivedAt`. Any send time
271−-- the sender wants to convey lives inside `encryptedBlob` (the server cannot
272−-- read it).
260+-- Storage TTL is fixed at `messageMaxAge`, so expiry is derived from
261+-- `receivedAt`. Any send time the sender wants to convey lives inside
262+-- `encryptedBlob` (the server cannot read it).
273263type Message = {toKey: PubkeyHex, encryptedBlob: BlobHex, receivedAt: Timestamp}
274264-- Last-known location of a pubkey, either local (`server == serverHostN`) or
275265-- learned from a peer via gossip. Multiple entries per pubkey are allowed.
@@ -391,14 +381,14 @@ route AuthApi where
391381
392382route ClientApi where
393383 POST {cursor: Timestamp, generation: Int} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp, authorized: Bool, rateLimited: Bool, generation: Int, heartbeat: Bool, retryAfterMs: Int, cursorReset: Bool, hasMore: Bool, cursorStale: Bool, loadPct: Int} = Poll
394− POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {results: [{messageId: MessageIdHex, error: Text}], error: Text, retryAfterMs: Int} = SendMessages
384+ POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {results: [{error: Text}], error: Text, retryAfterMs: Int} = SendMessages
395385 GET /lookup/{pubkey: PubkeyHex} headers {authorization: Text, clientIp: Text} -> {server: ServerName, authorized: Bool, error: Text} = LookupPresence
396386 POST {pubkeys: [PubkeyHex]} /lookup/batch headers {authorization: Text, clientIp: Text} -> {results: [{pubkey: PubkeyHex, server: ServerName}], authorized: Bool, error: Text} = BatchLookupPresence
397387
398388route FederationApi where
399389 POST {events: [GossipEvent], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/gossip headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = RecvGossip
400− POST {toKey: PubkeyHex, encryptedBlob: BlobHex, messageId: MessageIdHex, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
401− POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex, 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
390+ POST {toKey: PubkeyHex, encryptedBlob: BlobHex, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
391+ POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{ok: Bool}], retryAfterMs: Int} = FederationBatchForward
402392
403393route FederationPubkeyApi where
404394 GET /federation/pubkey -> {pubkey: Text, ok: Bool} = FederationPubkey
@@ -585,13 +575,6 @@ else do
585575genToken : IO {random} Text
586576genToken = genTokenN 48
587577
588−-- Canonical message identity: BLAKE3 of the encrypted blob, hex-encoded.
589−-- Binding the id to the payload means a hostile peer cannot multiply-store
590−-- the same ciphertext under different ids on /federation/forward — same
591−-- blob always collides with the existing dedup key.
592−messageIdOf : BlobHex -> MessageIdHex
593−messageIdOf = \blob -> bytesToHex (hash blob)
594−
595578-- True when `s` exceeds `maxLen` characters.
596579tooLong : Text -> Int -> Bool
597580tooLong = \s maxLen -> count (chars s) > maxLen
@@ -1020,12 +1003,10 @@ retryPendingForwards = do
10201003 blocked <- *blockedPeers
10211004 forEach (filter (collectPeers fwds (\f -> f.toServer)) (\s -> shouldRetryServer s t failures && not elem s blocked)) (\server -> forEach (takeRelation maxBatchSize (filter fwds (\f -> f.toServer == server))) (\f -> do
10221005 t2 <- now
1023− let messageId = messageIdOf f.encryptedBlob
1024− auth <- generateFederationAuth ("forward:" ++ messageId) t2
1006+ auth <- generateFederationAuth "forward" t2
10251007 result <- fetch (federationUrl server) (ForwardMessage {
10261008 f.toKey,
10271009 f.encryptedBlob,
1028− messageId,
10291010 fromServer: serverHostN,
10301011 requestTimestamp: t2,
10311012 authorization: auth,
@@ -1314,16 +1295,15 @@ ackPolledMessages = \pubkey cursor -> do
13141295-- the rest; federated forwards are forked per entry. Top-level `error`
13151296-- carries global gate failures ("rate_limited" | "unauthorized") and
13161297-- short-circuits with empty `results`.
1317−handleSendMessages : [{encryptedBlob: BlobHex, to: PubkeyHex | a}] -> Text -> Text -> IO {rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock} {error: Text, results: [{error: Text, messageId: MessageIdHex}], retryAfterMs: Int}
1298+handleSendMessages : [{encryptedBlob: BlobHex, to: PubkeyHex | a}] -> Text -> Text -> IO {rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock} {error: Text, results: [{error: Text}], retryAfterMs: Int}
13181299handleSendMessages = \messages authorization clientIp -> do
13191300 t <- now
13201301 let mkErr = \e -> {results: [], error: e, retryAfterMs: 0}
13211302 gateAuth authorization clientIp t mkErr (\senderKey -> do
13221303 results <- mapEach (takeRelation maxBatchSize messages) (\m -> do
1323− let effMsgId = messageIdOf m.encryptedBlob
13241304 r <- atomic (do sendMessageCore senderKey m.to m.encryptedBlob t)
13251305 when federationEnabled (fork (forkForwardsTo m.to m.encryptedBlob r.remoteServers))
1326− yield {messageId: effMsgId, r.error})
1306+ yield {r.error})
13271307 yield {results, error: "", retryAfterMs: 0})
13281308
13291309-- Atomic core of /messages. Charges send/pair/recv rate
@@ -1384,18 +1364,16 @@ sendMessageCore = \senderKey to encryptedBlob t -> do
13841364-- stays so `retryPendingForwards` can re-attempt with backoff.
13851365forkForwardsTo : PubkeyHex -> BlobHex -> [Text] -> IO {} {}
13861366forkForwardsTo = \to encryptedBlob servers -> forEach servers (\server -> do
1387− let effMsgId = messageIdOf encryptedBlob
13881367 fwds <- *forwards
13891368 let same = \x -> x.encryptedBlob == encryptedBlob && x.toServer == server
13901369 case findFirst fwds same of
13911370 Nothing {} -> yield {}
13921371 Just {value: f} -> do
13931372 t <- now
1394− auth <- generateFederationAuth ("forward:" ++ effMsgId) t
1373+ auth <- generateFederationAuth "forward" t
13951374 result <- fetch (federationUrl server) (ForwardMessage {
13961375 toKey: to,
13971376 encryptedBlob,
1398− messageId: effMsgId,
13991377 fromServer: serverHostN,
14001378 requestTimestamp: t,
14011379 authorization: auth,
@@ -1527,31 +1505,24 @@ storeFederated = \toKey encryptedBlob checkRecvRate t -> do
15271505
15281506-- POST /federation/forward: receive a single federated message destined for
15291507-- a local key. Delegates the actual store decision to `storeFederated`,
1530−-- which returns "" on success. The peer-supplied messageId in the request
1531−-- body is ignored: we derive the id from the blob ourselves so a hostile
1532−-- peer cannot influence the auth scope by varying the id. Honest senders
1533−-- derive the same value, so the "forward:" ++ messageId auth scope still
1534−-- verifies.
1535−handleForwardMessage : PubkeyHex -> BlobHex -> Text -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter, clock, network} {error: Text, ok: Bool, retryAfterMs: Int}
1536−handleForwardMessage = \toKey encryptedBlob _ fromServer requestTimestamp authorization -> do
1537− let messageId = messageIdOf encryptedBlob
1508+-- which returns "" on success.
1509+handleForwardMessage : PubkeyHex -> BlobHex -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter, clock, network} {error: Text, ok: Bool, retryAfterMs: Int}
1510+handleForwardMessage = \toKey encryptedBlob fromServer requestTimestamp authorization -> do
15381511 t <- now
1539− withFederationAuth authorization t (\e r -> {ok: False {}, error: e, retryAfterMs: r}) fromServer ("forward:" ++ messageId) requestTimestamp (isRemote fromServer) (do
1512+ withFederationAuth authorization t (\e r -> {ok: False {}, error: e, retryAfterMs: r}) fromServer "forward" requestTimestamp (isRemote fromServer) (do
15401513 result <- atomic storeFederated toKey encryptedBlob (True {}) t
15411514 yield {ok: result == "", error: result, retryAfterMs: 0})
15421515
15431516-- POST /federation/forward-batch: bulk variant of `handleForwardMessage`.
15441517-- Skips per-entry recv-rate charging (the per-server federation rate limit
15451518-- already gates the call as a whole).
1546−handleFederationBatchForward : [{encryptedBlob: BlobHex, messageId: MessageIdHex, toKey: PubkeyHex | a}] -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, clock, network} {error: Text, ok: Bool, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int}
1519+handleFederationBatchForward : [{encryptedBlob: BlobHex, toKey: PubkeyHex | a}] -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, clock, network} {error: Text, ok: Bool, results: [{ok: Bool}], retryAfterMs: Int}
15471520handleFederationBatchForward = \forwards fromServer requestTimestamp authorization -> do
15481521 t <- now
15491522 withFederationAuth authorization t (\e r -> {ok: False {}, error: e, results: [], retryAfterMs: r}) fromServer "forward-batch" requestTimestamp (isRemote fromServer && count forwards <= maxBatchSize) (do
15501523 results <- mapEach (takeRelation maxBatchSize forwards) (\e -> do
1551− -- Recompute id from blob; ignore e.messageId (untrusted, peer-chosen).
1552− let messageId = messageIdOf e.encryptedBlob
15531524 result <- atomic storeFederated e.toKey e.encryptedBlob (False {}) t
1554− yield {messageId, ok: result == ""})
1525+ yield {ok: result == ""})
15551526 yield {ok: True {}, error: "", results, retryAfterMs: 0})
15561527
15571528-- ============================================
@@ -2163,7 +2134,7 @@ api = serve Api where
21632134 LookupPresence = \{pubkey, authorization, clientIp} -> handleLookupPresence pubkey authorization clientIp
21642135 BatchLookupPresence = \{pubkeys, authorization, clientIp} -> handleBatchLookupPresence pubkeys authorization clientIp
21652136 RecvGossip = \{events, fromServer, requestTimestamp, authorization} -> handleRecvGossip events fromServer requestTimestamp authorization
2166− ForwardMessage = \{toKey, encryptedBlob, messageId, fromServer, requestTimestamp, authorization} -> handleForwardMessage toKey encryptedBlob messageId fromServer requestTimestamp authorization
2137+ ForwardMessage = \{toKey, encryptedBlob, fromServer, requestTimestamp, authorization} -> handleForwardMessage toKey encryptedBlob fromServer requestTimestamp authorization
21672138 FederationBatchForward = \{forwards, fromServer, requestTimestamp, authorization} -> handleFederationBatchForward forwards fromServer requestTimestamp authorization
21682139 FederationPubkey = \{} -> handleFederationPubkey
21692140 Prune = \{authorization} -> handlePrune authorization