on master help sign in
Always generate message IDs server-side; remove from client send API
commitc6fb6ddfda16ebf43673f0e17a6fdd8444fab1fa authoriko <mail@iko.soy> dateSun 3 May 2026, 15:18 (5 months ago) parentdbdd8aa
1 file changed+9−13
server.knot+9−13
server.knot+9−13
@@ -294,7 +294,8 @@ isValidMessageId : Text -> Bool
294294isValidMessageId = isValidBoundedHex maxMessageIdLen
295295-- Hex-encoded message id, length ≤ `maxMessageIdLen`.
296296type MessageIdHex = Text where isValidMessageId
297−-- A `MessageIdHex` or the empty string. Empty means "let the server generate one".
297+-- A `MessageIdHex` or the empty string. Used in send responses, where empty
298+-- means the send was rejected so no id was assigned.
298299type OptionalMessageId = Text where \s -> s == "" || isValidMessageId s
299300-- Normalized hostname (lowercased, no trailing dot, length-bounded).
300301isAnyServerName : Text -> Bool
@@ -397,7 +398,6 @@ type BatchMessage = {
397398 to: PubkeyHex,
398399 encryptedBlob: BlobHex,
399400 timestamp: Timestamp,
400− messageId: OptionalMessageId,
401401 expiresInMs: Int
402402}
403403-- Per-entry result returned by /messages/batch.
@@ -481,7 +481,7 @@ route AuthApi where
481481
482482route ClientApi where
483483 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, messageExpirations: [{messageId: MessageIdHex, toKey: PubkeyHex, expiredAt: Timestamp}], messageDeliveries: [DeliveredEntry], droppedDeliveryReceipts: Int, droppedExpirationNotifs: Int, loadPct: Int} = Poll
484− POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, messageId: OptionalMessageId, expiresInMs: Int} /messages headers {authorization: Text, clientIp: Text} -> {status: MessageStatus, messageId: OptionalMessageId, retryAfterMs: Int, error: Text, loadPct: Int} = SendMessage
484+ POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, expiresInMs: Int} /messages headers {authorization: Text, clientIp: Text} -> {status: MessageStatus, messageId: OptionalMessageId, retryAfterMs: Int, error: Text, loadPct: Int} = SendMessage
485485 POST {to: PubkeyHex, messageId: MessageIdHex} /messages/status headers {authorization: Text, clientIp: Text} -> {status: Text, authorized: Bool} = MessageStatus
486486 POST {messageIds: [MessageIdHex]} /messages/ack headers {authorization: Text, clientIp: Text} -> {ok: Bool, authorized: Bool, ackedCount: Int, error: Text} = AckMessages
487487 POST {messages: [BatchMessage]} /messages/batch headers {authorization: Text, clientIp: Text} -> {results: [BatchResult], authorized: Bool, retryAfterMs: Int} = SendBatch
@@ -704,10 +704,6 @@ else do
704704genToken : IO {random} Text
705705genToken = genTokenN 48
706706
707−-- Use the supplied message id when non-empty, otherwise generate a fresh one.
708−resolveMessageId : OptionalMessageId -> IO {random} OptionalMessageId
709−resolveMessageId = \mid -> if mid != "" then yield mid else genToken
710−
711707-- True when `s` exceeds `maxLen` characters.
712708tooLong : Text -> Int -> Bool
713709tooLong = \s maxLen -> count (chars s) > maxLen
@@ -1617,12 +1613,12 @@ ackPolledMessages = \pubkey cursor t -> do
16171613-- POST /messages: send one ciphertext to `to`. Rejects on draining/rate
16181614-- limit/stale timestamp; otherwise calls `sendMessageCore` inside an atomic
16191615-- block and forks the federation forward (if any) once the local commit lands.
1620−handleSendMessage : PubkeyHex -> BlobHex -> Int<Ms> -> OptionalMessageId -> Int -> Text -> Text -> ({error: Text, loadPct: Int, messageId: OptionalMessageId, retryAfterMs: Int, status: MessageStatus} -> a) -> IO {r *drainPhase, rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock, random} a
1621−handleSendMessage = \to encryptedBlob timestamp messageId expiresInMs authorization clientIp respond -> do
1616+handleSendMessage : PubkeyHex -> BlobHex -> Int<Ms> -> Int -> Text -> Text -> ({error: Text, loadPct: Int, messageId: OptionalMessageId, retryAfterMs: Int, status: MessageStatus} -> a) -> IO {r *drainPhase, rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock, random} a
1617+handleSendMessage = \to encryptedBlob timestamp expiresInMs authorization clientIp respond -> do
16221618 t <- now
16231619 dp <- *drainPhase
16241620 g <- checkGlobalRate t
1625− effMsgId <- resolveMessageId messageId
1621+ effMsgId <- genToken
16261622 let err = \s e -> yield (respond (sendErrResp s e))
16271623 if dp >= 2
16281624 then err (MsDraining {}) "draining"
@@ -1895,13 +1891,13 @@ handleDeleteMessages = \messageIds authorization clientIp respond -> do
18951891-- POST /messages/batch: multi-recipient send in one call. Each entry runs
18961892-- through `sendMessageCore` separately so per-entry rate limits/errors don't
18971893-- abort the rest; federated forwards are forked per entry.
1898−handleSendBatch : [{encryptedBlob: BlobHex, expiresInMs: Int, messageId: OptionalMessageId, 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
1894+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
18991895handleSendBatch = \messages authorization clientIp respond -> do
19001896 t <- now
19011897 let mkErr = \_ -> {results: [], authorized: False {}, retryAfterMs: 0}
19021898 gateAuth (True {}) authorization clientIp t respond mkErr (\senderKey -> do
19031899 results <- mapEach (takeRelation maxBatchSize messages) (\m -> do
1904− effMsgId <- resolveMessageId m.messageId
1900+ effMsgId <- genToken
19051901 let clampedTs = clampTimestamp m.timestamp t
19061902 r <- if t - m.timestamp > messageMaxAge
19071903 then yield {status: MsRejected {}, remoteServers: []}
@@ -2815,7 +2811,7 @@ serve = \req -> case req of
28152811 RevokeSession {targetToken, authorization, clientIp, respond} -> handleRevoke targetToken authorization clientIp respond
28162812 Logout {authorization, clientIp, respond} -> handleLogout authorization clientIp respond
28172813 Poll {cursor, generation, authorization, clientIp, respond} -> handlePoll cursor generation authorization clientIp respond
2818− SendMessage {to, encryptedBlob, timestamp, messageId, expiresInMs, authorization, clientIp, respond} -> handleSendMessage to encryptedBlob timestamp messageId expiresInMs authorization clientIp respond
2814+ SendMessage {to, encryptedBlob, timestamp, expiresInMs, authorization, clientIp, respond} -> handleSendMessage to encryptedBlob timestamp expiresInMs authorization clientIp respond
28192815 MessageStatus {to, messageId, authorization, clientIp, respond} -> handleMessageStatus to messageId authorization clientIp respond
28202816 AckMessages {messageIds, authorization, clientIp, respond} -> handleAckMessages messageIds authorization clientIp respond
28212817 SendBatch {messages, authorization, clientIp, respond} -> handleSendBatch messages authorization clientIp respond