on master help sign in
Persist federation keypair to SQLite, remove Bearer token fallback
806042eiko
server.knot+102−67
@@ -11,162 +11,225 @@
11-- per-key/window rate limits, expiry/backoff/TTL durations, and protocol version.
11-- per-key/window rate limits, expiry/backoff/TTL durations, and protocol version.
12-- All values are plain literals; nothing is read from disk or env at runtime.
12-- All values are plain literals; nothing is read from disk or env at runtime.
13
13
14+-- Hostname this server binds to. Use 0.0.0.0 to accept connections on every interface.
14serverHost : Text
15serverHost : Text
15serverHost = "localhost"
16serverHost = "localhost"
17+-- TCP port for incoming HTTP connections.
16listenPort : Int
18listenPort : Int
17listenPort = 8080
19listenPort = 8080
20+-- URL scheme used when contacting peer federation servers.
18federationScheme : Text
21federationScheme : Text
19federationScheme = "https://"
22federationScheme = "https://"
20−serverFederationPrivateKeyHex : Text
23+-- The Ed25519 federation keypair lives in `*federationPrivateKeyHex` /
21−serverFederationPrivateKeyHex = ""
24+-- `*federationPublicKeyHex` refs and is generated on first startup; see `main`.
22−serverFederationPublicKeyHex : Text
23−serverFederationPublicKeyHex = ""
24
25
26+-- Drop stored messages older than this (30 days).
25messageMaxAge : Int<Ms>
27messageMaxAge : Int<Ms>
26messageMaxAge = 2592000000
28messageMaxAge = 2592000000
29+-- Upper bound on TTL a client may request for a presence entry (1 hour).
27maxPresenceTtl : Int<Ms>
30maxPresenceTtl : Int<Ms>
28maxPresenceTtl = 3600000
31maxPresenceTtl = 3600000
32+-- Max length (chars) of a hex-encoded pubkey accepted on the wire.
29maxPubkeyLen : Int
33maxPubkeyLen : Int
30maxPubkeyLen = 256
34maxPubkeyLen = 256
35+-- Max length of a hex-encoded message body / opaque blob (64 KiB hex ≈ 32 KiB bytes).
31maxBlobLen : Int
36maxBlobLen : Int
32maxBlobLen = 65536
37maxBlobLen = 65536
38+-- Max length of a hex-encoded signature.
33maxSignatureLen : Int
39maxSignatureLen : Int
34maxSignatureLen = 256
40maxSignatureLen = 256
41+-- Cap on the number of message IDs accepted in a single ack/lookup request.
35maxMessageIds : Int
42maxMessageIds : Int
36maxMessageIds = 100
43maxMessageIds = 100
44+-- Cap on items in a batched send or batched cancel request.
37maxBatchSize : Int
45maxBatchSize : Int
38maxBatchSize = 100
46maxBatchSize = 100
47+-- Highest priority value a message is allowed to declare.
39maxMessagePriority : Int
48maxMessagePriority : Int
40maxMessagePriority = 10
49maxMessagePriority = 10
50+-- Cap on gossip events accepted or emitted in a single federation exchange.
41maxGossipEvents : Int
51maxGossipEvents : Int
42maxGossipEvents = 100
52maxGossipEvents = 100
53+-- Cap on events returned by one long-poll response.
43maxPollEvents : Int
54maxPollEvents : Int
44maxPollEvents = 50
55maxPollEvents = 50
56+-- Hard ceiling on presence entries kept in memory.
45maxPresenceEntries : Int
57maxPresenceEntries : Int
46maxPresenceEntries = 10000
58maxPresenceEntries = 10000
59+-- Per-recipient ceiling on stored undelivered messages before older ones are pruned.
47maxMessagesPerRecipient : Int
60maxMessagesPerRecipient : Int
48maxMessagesPerRecipient = 500
61maxMessagesPerRecipient = 500
62+-- Total ceiling on stored messages across all recipients.
49maxMessages : Int
63maxMessages : Int
50maxMessages = 100000
64maxMessages = 100000
65+-- Allowance for clock drift when validating client timestamps (5 s).
51clockSkewTolerance : Int<Ms>
66clockSkewTolerance : Int<Ms>
52clockSkewTolerance = 5000
67clockSkewTolerance = 5000
68+-- Ceiling on outstanding federation forward attempts retained for retry.
53maxForwardsEntries : Int
69maxForwardsEntries : Int
54maxForwardsEntries = 10000
70maxForwardsEntries = 10000
71+-- Per-peer ceiling on outstanding forward attempts.
55maxForwardsPerServer : Int
72maxForwardsPerServer : Int
56maxForwardsPerServer = 2000
73maxForwardsPerServer = 2000
74+-- Max accepted length of a server hostname.
57maxServerNameLen : Int
75maxServerNameLen : Int
58maxServerNameLen = 128
76maxServerNameLen = 128
77+-- Max accepted length of a hex-encoded message ID.
59maxMessageIdLen : Int
78maxMessageIdLen : Int
60maxMessageIdLen = 128
79maxMessageIdLen = 128
80+-- Max accepted length of an auth/session token.
61maxTokenLen : Int
81maxTokenLen : Int
62maxTokenLen = 256
82maxTokenLen = 256
83+-- Ceiling on outstanding auth challenges held in memory.
63maxChallenges : Int
84maxChallenges : Int
64maxChallenges = 10000
85maxChallenges = 10000
86+-- Ceiling on concurrent active sessions.
65maxSessions : Int
87maxSessions : Int
66maxSessions = 10000
88maxSessions = 10000
89+-- Ceiling on remembered acked message IDs (used for replay protection).
67maxAckedIds : Int
90maxAckedIds : Int
68maxAckedIds = 10000
91maxAckedIds = 10000
92+-- Ceiling on remembered delivered message IDs.
69maxDeliveredIds : Int
93maxDeliveredIds : Int
70maxDeliveredIds = 10000
94maxDeliveredIds = 10000
95+-- Per-pubkey ceiling on outstanding challenges (prevents one key from exhausting the pool).
71maxChallengesPerKey : Int
96maxChallengesPerKey : Int
72maxChallengesPerKey = 5
97maxChallengesPerKey = 5
98+-- Per-pubkey ceiling on concurrent sessions.
73maxSessionsPerKey : Int
99maxSessionsPerKey : Int
74maxSessionsPerKey = 10
100maxSessionsPerKey = 10
101+-- Validity window for an auth challenge before it must be re-issued (1 min).
75challengeExpiry : Int<Ms>
102challengeExpiry : Int<Ms>
76challengeExpiry = 60000
103challengeExpiry = 60000
104+-- Challenge requests permitted per key inside its rate window.
77maxChallengeRatePerKey : Int
105maxChallengeRatePerKey : Int
78maxChallengeRatePerKey = 5
106maxChallengeRatePerKey = 5
107+-- Window over which the per-key challenge rate is measured (1 s).
79challengeRateWindowMs : Int<Ms>
108challengeRateWindowMs : Int<Ms>
80challengeRateWindowMs = 1000
109challengeRateWindowMs = 1000
110+-- Send-message requests permitted per key per window.
81maxSendRatePerKey : Int
111maxSendRatePerKey : Int
82maxSendRatePerKey = 20
112maxSendRatePerKey = 20
113+-- Window for the per-key send rate limit (1 s).
83sendRateWindowMs : Int<Ms>
114sendRateWindowMs : Int<Ms>
84sendRateWindowMs = 1000
115sendRateWindowMs = 1000
116+-- Receive/poll requests permitted per key per window.
85maxRecvRatePerKey : Int
117maxRecvRatePerKey : Int
86maxRecvRatePerKey = 10
118maxRecvRatePerKey = 10
119+-- Window for the per-key receive rate limit (1 s).
87recvRateWindowMs : Int<Ms>
120recvRateWindowMs : Int<Ms>
88recvRateWindowMs = 1000
121recvRateWindowMs = 1000
122+-- Messages permitted per (sender, recipient) pair within the pair window.
89maxPairRatePerPair : Int
123maxPairRatePerPair : Int
90maxPairRatePerPair = 30
124maxPairRatePerPair = 30
125+-- Window for the per-pair rate limit (1 min).
91pairRateWindowMs : Int<Ms>
126pairRateWindowMs : Int<Ms>
92pairRateWindowMs = 60000
127pairRateWindowMs = 60000
128+-- Federation requests permitted from a single peer server per window.
93maxFederationRatePerServer : Int
129maxFederationRatePerServer : Int
94maxFederationRatePerServer = 50
130maxFederationRatePerServer = 50
131+-- Window for the per-peer federation rate limit (1 s).
95federationRateWindowMs : Int<Ms>
132federationRateWindowMs : Int<Ms>
96federationRateWindowMs = 1000
133federationRateWindowMs = 1000
134+-- Presence/key lookups permitted per key per window.
97maxLookupRatePerKey : Int
135maxLookupRatePerKey : Int
98maxLookupRatePerKey = 5
136maxLookupRatePerKey = 5
137+-- Window for the per-key lookup rate limit (1 s).
99lookupRateWindowMs : Int<Ms>
138lookupRateWindowMs : Int<Ms>
100lookupRateWindowMs = 1000
139lookupRateWindowMs = 1000
140+-- Other client write requests permitted per key per window.
101maxClientWriteRatePerKey : Int
141maxClientWriteRatePerKey : Int
102maxClientWriteRatePerKey = 10
142maxClientWriteRatePerKey = 10
143+-- Window for the per-key client-write rate limit (1 s).
103clientWriteRateWindowMs : Int<Ms>
144clientWriteRateWindowMs : Int<Ms>
104clientWriteRateWindowMs = 1000
145clientWriteRateWindowMs = 1000
146+-- Requests permitted per source IP per window (covers unauthenticated traffic).
105maxIpRatePerIp : Int
147maxIpRatePerIp : Int
106maxIpRatePerIp = 10
148maxIpRatePerIp = 10
149+-- Window for the per-IP rate limit (1 s).
107ipRateWindowMs : Int<Ms>
150ipRateWindowMs : Int<Ms>
108ipRateWindowMs = 1000
151ipRateWindowMs = 1000
152+-- Global ceiling on total request rate across all clients.
109maxGlobalRequestRate : Int
153maxGlobalRequestRate : Int
110maxGlobalRequestRate = 1000
154maxGlobalRequestRate = 1000
155+-- Window for the global request rate limit (1 s).
111globalRateWindowMs : Int<Ms>
156globalRateWindowMs : Int<Ms>
112globalRateWindowMs = 1000
157globalRateWindowMs = 1000
158+-- Global ceiling on auth-endpoint requests (challenge + verify combined).
113maxAuthGlobalRate : Int
159maxAuthGlobalRate : Int
114maxAuthGlobalRate = 200
160maxAuthGlobalRate = 200
161+-- Window for the global auth rate limit (1 s).
115authGlobalRateWindowMs : Int<Ms>
162authGlobalRateWindowMs : Int<Ms>
116authGlobalRateWindowMs = 1000
163authGlobalRateWindowMs = 1000
117
164
165+-- How long a session token remains valid after issue (1 hour).
118sessionExpiry : Int<Ms>
166sessionExpiry : Int<Ms>
119sessionExpiry = 3600000
167sessionExpiry = 3600000
168+-- How long an "online" presence gossip is considered fresh (90 min).
120onlineGossipTtl : Int<Ms>
169onlineGossipTtl : Int<Ms>
121onlineGossipTtl = 5400000
170onlineGossipTtl = 5400000
171+-- Floor on TTL accepted for "offline" presence gossip (1 min).
122offlineGossipMinTtl : Int<Ms>
172offlineGossipMinTtl : Int<Ms>
123offlineGossipMinTtl = 60000
173offlineGossipMinTtl = 60000
174+-- Times to retry a federation forward before giving up.
124maxForwardRetries : Int
175maxForwardRetries : Int
125maxForwardRetries = 10
176maxForwardRetries = 10
177+-- Hop-count ceiling for gossip propagation between peers.
126maxGossipHops : Int
178maxGossipHops : Int
127maxGossipHops = 5
179maxGossipHops = 5
180+-- Initial hop budget assigned to gossip events that originate on this server.
128gossipOriginHops : Int
181gossipOriginHops : Int
129gossipOriginHops = 2
182gossipOriginHops = 2
130
183
184+-- Base delay for federation retry backoff (linear, not exponential — 30 s).
131retryBackoffBase : Int<Ms>
185retryBackoffBase : Int<Ms>
132retryBackoffBase = 30000
186retryBackoffBase = 30000
187+-- Cap on federation retry backoff (8 min).
133retryBackoffMax : Int<Ms>
188retryBackoffMax : Int<Ms>
134retryBackoffMax = 480000
189retryBackoffMax = 480000
135
190
136−verificationTokenExpiry : Int<Ms>
191+-- Cadence of the background prune/cleanup task (30 s).
137−verificationTokenExpiry = 30000
138−maxPendingVerifications : Int
139−maxPendingVerifications = 10000
140−
141backgroundPruneInterval : Int<Ms>
192backgroundPruneInterval : Int<Ms>
142backgroundPruneInterval = 30000
193backgroundPruneInterval = 30000
194+-- Long-poll hold timeout before returning empty to the client (25 s).
143pollTimeoutMs : Int<Ms>
195pollTimeoutMs : Int<Ms>
144pollTimeoutMs = 25000
196pollTimeoutMs = 25000
197+-- Concurrent active long-polls permitted per pubkey.
145maxPollsPerKey : Int
198maxPollsPerKey : Int
146maxPollsPerKey = 3
199maxPollsPerKey = 3
200+-- Global ceiling on active long-polls.
147maxActivePolls : Int
201maxActivePolls : Int
148maxActivePolls = 5000
202maxActivePolls = 5000
203+-- Max number of pubkeys a single poll may subscribe to.
149maxWatchKeys : Int
204maxWatchKeys : Int
150maxWatchKeys = 50
205maxWatchKeys = 50
206+-- Cadence at which this server refreshes its own presence to peers (5 min).
151presenceRefreshInterval : Int<Ms>
207presenceRefreshInterval : Int<Ms>
152presenceRefreshInterval = 300000
208presenceRefreshInterval = 300000
153
209
210+-- Drop unacked messages older than this (7 days).
154unackedMessageMaxAge : Int<Ms>
211unackedMessageMaxAge : Int<Ms>
155unackedMessageMaxAge = 604800000
212unackedMessageMaxAge = 604800000
213+-- Floor on the TTL a client may request when sending a message (1 min).
156minMessageTtl : Int<Ms>
214minMessageTtl : Int<Ms>
157minMessageTtl = 60000
215minMessageTtl = 60000
216+-- Window during which a delivered-but-unacked message is retained for re-delivery (2 min).
158ackGracePeriod : Int<Ms>
217ackGracePeriod : Int<Ms>
159ackGracePeriod = 120000
218ackGracePeriod = 120000
160
219
220+-- Wire protocol version used for federation calls; bumped on incompatible changes.
161federationProtocolVersion : Int
221federationProtocolVersion : Int
162federationProtocolVersion = 15
222federationProtocolVersion = 15
223+-- Human-readable build identifier returned by the /health endpoint.
163serverBuildVersion : Text
224serverBuildVersion : Text
164serverBuildVersion = "2026-04-27-simple"
225serverBuildVersion = "2026-04-27-simple"
165
226
227+-- Master switch for federation; when False the server runs in single-node mode.
166federationEnabled : Bool
228federationEnabled : Bool
167federationEnabled = True {}
229federationEnabled = True {}
168federationUrl : Text -> Text
230federationUrl : Text -> Text
169federationUrl = \server -> federationScheme ++ server
231federationUrl = \server -> federationScheme ++ server
232+-- When True, reject requests that arrive without a client-IP header (for IP-based rate limits).
170requireClientIp : Bool
233requireClientIp : Bool
171requireClientIp = True {}
234requireClientIp = True {}
172
235
@@ -376,8 +439,6 @@ type BatchCancelResult = {messageId: MessageIdHex, cancelled: Bool}
376*drainPhase : Int
439*drainPhase : Int
377*drainCompletedAt : Timestamp
440*drainCompletedAt : Timestamp
378
441
379−*pendingVerifications : [{token: Text, createdAt: Timestamp, scope: Text}]
380−
381*senderExpirations : [SenderExpiration]
442*senderExpirations : [SenderExpiration]
382*senderDeliveries : [SenderDelivery]
443*senderDeliveries : [SenderDelivery]
383*cancelNotifications : [CancelNotification]
444*cancelNotifications : [CancelNotification]
@@ -385,6 +446,9 @@ type BatchCancelResult = {messageId: MessageIdHex, cancelled: Bool}
385
446
386*runtimeMaintenanceSecret : Text
447*runtimeMaintenanceSecret : Text
387
448
449+*federationPrivateKeyHex : Text
450+*federationPublicKeyHex : Text
451+
388*blockedPeers : [Text]
452*blockedPeers : [Text]
389*allowedPeers : [Text]
453*allowedPeers : [Text]
390
454
@@ -398,7 +462,6 @@ type BatchCancelResult = {messageId: MessageIdHex, cancelled: Bool}
398-- AuthApi -- challenge/verify/revoke/logout (client identity)
462-- AuthApi -- challenge/verify/revoke/logout (client identity)
399-- ClientApi -- /poll, send/batch/list/ack/delete/cancel, presence lookup
463-- ClientApi -- /poll, send/batch/list/ack/delete/cancel, presence lookup
400-- FederationApi -- inter-server gossip, forward, ack, cancel, delivery receipts
464-- FederationApi -- inter-server gossip, forward, ack, cancel, delivery receipts
401−-- FederationVerify -- callback used by peers to verify Bearer tokens this server issued
402-- FederationPubkey -- exposes this server's federation Ed25519 public key
465-- FederationPubkey -- exposes this server's federation Ed25519 public key
403-- MaintenanceApi -- operator endpoints (drain, prune, peer allow/block, admin)
466-- MaintenanceApi -- operator endpoints (drain, prune, peer allow/block, admin)
404-- HealthApi/StatsApi/MetricsApi/ReadyApi -- observability
467-- HealthApi/StatsApi/MetricsApi/ReadyApi -- observability
@@ -433,9 +496,6 @@ route FederationApi where
433 POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: ServerName, messageId: MessageIdHex, expiresAt: Timestamp, priority: Int}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text, protocolVersion: Int} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} = FederationBatchForward
496 POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: ServerName, messageId: MessageIdHex, expiresAt: Timestamp, priority: Int}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text, protocolVersion: Int} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} = FederationBatchForward
434 POST {messageIds: [MessageIdHex], toKeys: [PubkeyHex], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text, protocolVersion: Int} /federation/delivery-receipt headers {authorization: Text} -> {ok: Bool, error: Text, receivedCount: Int, retryAfterMs: Int} = FederationDeliveryReceipt
497 POST {messageIds: [MessageIdHex], toKeys: [PubkeyHex], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text, protocolVersion: Int} /federation/delivery-receipt headers {authorization: Text} -> {ok: Bool, error: Text, receivedCount: Int, retryAfterMs: Int} = FederationDeliveryReceipt
435
498
436−route FederationVerifyApi where
437− POST {token: Text, scope: Text} /federation/verify headers {clientIp: Text} -> {ok: Bool} = VerifyToken
438−
439route FederationPubkeyApi where
499route FederationPubkeyApi where
440 GET /federation/pubkey -> {pubkey: Text, ok: Bool} = FederationPubkey
500 GET /federation/pubkey -> {pubkey: Text, ok: Bool} = FederationPubkey
441
501
@@ -496,7 +556,7 @@ route MetricsApi where
496route ReadyApi where
556route ReadyApi where
497 GET /ready -> {ready: Bool} = Ready
557 GET /ready -> {ready: Bool} = Ready
498
558
499−route Api = AuthApi | ClientApi | FederationApi | FederationVerifyApi | FederationPubkeyApi | MaintenanceApi | HealthApi | StatsApi | MetricsApi | ReadyApi
559+route Api = AuthApi | ClientApi | FederationApi | FederationPubkeyApi | MaintenanceApi | HealthApi | StatsApi | MetricsApi | ReadyApi
500
560
501-- ============================================
561-- ============================================
502-- Generic helpers
562-- Generic helpers
@@ -691,8 +751,8 @@ isBadServerName = \sn -> sn == "" || sn == "localhost" || not all (chars sn) (\c
691-- Bearer token extraction, session lookup, maintenance secret check, and the
751-- Bearer token extraction, session lookup, maintenance secret check, and the
692-- `gateAuth`/`gateClientWrite`/`gateLookup` combinators that every client
752-- `gateAuth`/`gateClientWrite`/`gateLookup` combinators that every client
693-- handler funnels through (drain → global rate → per-key rate → session).
753-- handler funnels through (drain → global rate → per-key rate → session).
694−-- Federation requests use a separate path (`withFederationAuth`) supporting
754+-- Federation requests use a separate path (`withFederationAuth`) using
695−-- both Ed25519-signed and callback-Bearer auth.
755+-- Ed25519-signed auth verified via the peer's published public key.
696
756
697-- Plain ip match (sessions with empty ip skip the check).
757-- Plain ip match (sessions with empty ip skip the check).
698matchIp : Text -> Text -> Bool
758matchIp : Text -> Text -> Bool
@@ -756,24 +816,17 @@ withSessionAuth = \authorization t clientIp onUnauth onAuth -> do
756 Just {value: s} -> onAuth s.pubkey
816 Just {value: s} -> onAuth s.pubkey
757 _ -> onUnauth
817 _ -> onUnauth
758
818
759−generateFederationAuth : Text -> a -> IO {reads *pendingVerifications, writes *pendingVerifications, clock, random} Text
819+generateFederationAuth : Text -> a -> IO {reads *federationPrivateKeyHex, random} Text
760generateFederationAuth = \scope requestTimestamp -> do
820generateFederationAuth = \scope requestTimestamp -> do
761− let privKey = if serverFederationPrivateKeyHex == ""
821+ hex <- *federationPrivateKeyHex
762− then Nothing {}
822+ let privKey = if hex == "" then Nothing {} else bytesFromHex hex
763− else bytesFromHex serverFederationPrivateKeyHex
764 case privKey of
823 case privKey of
765 Just {value: pk} -> do
824 Just {value: pk} -> do
766 nonce <- genTokenN 48
825 nonce <- genTokenN 48
767 yield ("Signed " ++ bytesToHex (sign pk (textToBytes (scope ++ "|" ++ show requestTimestamp ++ "|" ++ serverHostN ++ "|" ++ nonce))) ++ ":" ++ nonce)
826 yield ("Signed " ++ bytesToHex (sign pk (textToBytes (scope ++ "|" ++ show requestTimestamp ++ "|" ++ serverHostN ++ "|" ++ nonce))) ++ ":" ++ nonce)
768− _ -> do
769− token <- genToken
770− t <- now
771− atomic (do
772− cur <- *pendingVerifications
773− *pendingVerifications = capList (union cur [{token, createdAt: t, scope}]) maxPendingVerifications (\p -> p.createdAt))
774− yield ("Bearer " ++ token)
827+ _ -> yield ""
775
828
776−-- Verify a federation request: signed (Ed25519 over scope|ts|fromServer|nonce) or callback Bearer token.
829+-- Verify a federation request: Ed25519 signature over scope|ts|fromServer|nonce, key fetched from peer.
777verifyFederationAuth : Text -> Text -> Text -> a -> IO {network} {} Bool
830verifyFederationAuth : Text -> Text -> Text -> a -> IO {network} {} Bool
778verifyFederationAuth = \authorization fromServer scope requestTimestamp -> do
831verifyFederationAuth = \authorization fromServer scope requestTimestamp -> do
779 if hasPrefix "Signed " authorization then do
832 if hasPrefix "Signed " authorization then do
@@ -795,11 +848,7 @@ verifyFederationAuth = \authorization fromServer scope requestTimestamp -> do
795 _ -> yield False {}
848 _ -> yield False {}
796 _ -> yield False {}
849 _ -> yield False {}
797 _ -> yield False {}
850 _ -> yield False {}
798− else case extractToken authorization of
799− Just {value: token} -> do
800− result <- fetch (federationUrl fromServer) (VerifyToken {token: token, scope: scope, clientIp: ""})
801− case result of Ok {value: r} -> yield r.ok; _ -> yield False {}
802− _ -> yield False {}
851+ else yield False {}
803
852
804withFederationAuth = \authorization t respond fromServer scope requestTimestamp precondition action -> do
853withFederationAuth = \authorization t respond fromServer scope requestTimestamp precondition action -> do
805 blocked <- *blockedPeers
854 blocked <- *blockedPeers
@@ -985,7 +1034,7 @@ mergePresence = \existing incoming -> fold (\acc e -> (\same -> if any acc (\x -
985-- block. Returns the locally-expired presence rows whose pubkey has no other
1034-- block. Returns the locally-expired presence rows whose pubkey has no other
986-- live local session, so the caller can broadcast offline gossip for them.
1035-- live local session, so the caller can broadcast offline gossip for them.
987
1036
988−pruneExpired : Int<Ms> -> IO {reads *ackedIds, reads *activePolls, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *forwardFailures, reads *forwards, reads *messages, reads *pendingFederationCancels, reads *pendingVerifications, reads *presence, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *messages, writes *pendingFederationCancels, writes *pendingVerifications, writes *presence, writes *senderDeliveries, writes *senderExpirations, writes *sessions} [{expiresAt: Timestamp, pubkey: PubkeyHex, server: AnyServerName, timestamp: Timestamp}]
1037+pruneExpired : Int<Ms> -> IO {reads *ackedIds, reads *activePolls, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *forwardFailures, reads *forwards, reads *messages, reads *pendingFederationCancels, reads *presence, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *messages, writes *pendingFederationCancels, writes *presence, writes *senderDeliveries, writes *senderExpirations, writes *sessions} [{expiresAt: Timestamp, pubkey: PubkeyHex, server: AnyServerName, timestamp: Timestamp}]
989pruneExpired = \t -> atomic do
1038pruneExpired = \t -> atomic do
990 ch <- *challenges
1039 ch <- *challenges
991 *challenges = filter ch (\x -> x.expiresAt > t)
1040 *challenges = filter ch (\x -> x.expiresAt > t)
@@ -1003,8 +1052,6 @@ pruneExpired = \t -> atomic do
1003 *forwardFailures = filter ff (\f -> t - f.failedAt < 86400000)
1052 *forwardFailures = filter ff (\f -> t - f.failedAt < 86400000)
1004 aps <- *activePolls
1053 aps <- *activePolls
1005 *activePolls = filter aps (\p -> t - p.startedAt < pollTimeoutMs * 3)
1054 *activePolls = filter aps (\p -> t - p.startedAt < pollTimeoutMs * 3)
1006− pv <- *pendingVerifications
1007− *pendingVerifications = filter pv (\p -> t - p.createdAt < verificationTokenExpiry)
1008 se <- *senderExpirations
1055 se <- *senderExpirations
1009 *senderExpirations = filter se (\x -> t - x.expiredAt < unackedMessageMaxAge)
1056 *senderExpirations = filter se (\x -> t - x.expiredAt < unackedMessageMaxAge)
1010 sd <- *senderDeliveries
1057 sd <- *senderDeliveries
@@ -1217,7 +1264,7 @@ forwardQueuedToServer = \pubkey server t -> atomic (do
1217-- expire stale rows, then opportunistically retry pending forwards and refresh
1264-- expire stale rows, then opportunistically retry pending forwards and refresh
1218-- local presence so peers don't see us go offline mid-session.
1265-- local presence so peers don't see us go offline mid-session.
1219
1266
1220−pruneAndRetry : Int<Ms> -> IO {reads *ackedIds, reads *activePolls, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *forwardFailures, reads *forwards, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *pendingVerifications, reads *presence, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *pendingVerifications, writes *presence, writes *senderDeliveries, writes *senderExpirations, writes *sessions, reads *blockedPeers, reads *lastPresenceRefresh, writes *lastPresenceRefresh, clock} {}
1267+pruneAndRetry : Int<Ms> -> IO {reads *ackedIds, reads *activePolls, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *federationPrivateKeyHex, reads *forwardFailures, reads *forwards, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *presence, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *presence, writes *senderDeliveries, writes *senderExpirations, writes *sessions, reads *blockedPeers, reads *lastPresenceRefresh, writes *lastPresenceRefresh, clock} {}
1221pruneAndRetry = \t -> do
1268pruneAndRetry = \t -> do
1222 *lastPrunedAt = t
1269 *lastPrunedAt = t
1223 expiredLocal <- pruneExpired t
1270 expiredLocal <- pruneExpired t
@@ -1920,35 +1967,17 @@ handleSendBatch = \messages authorization clientIp respond -> do
1920-- Inbound endpoints called peer-to-peer. Every handler funnels through
1967-- Inbound endpoints called peer-to-peer. Every handler funnels through
1921-- `withFederationAuth` which checks: peer block/allow lists, request
1968-- `withFederationAuth` which checks: peer block/allow lists, request
1922-- precondition (e.g. expected origin/server constraints), clock skew vs the
1969-- precondition (e.g. expected origin/server constraints), clock skew vs the
1923−-- request timestamp, signature/Bearer auth, and per-peer rate limit.
1970+-- request timestamp, Ed25519 signature auth, and per-peer rate limit.
1924-- Receive paths never trust `originServer` for routing; they only use it for
1971-- Receive paths never trust `originServer` for routing; they only use it for
1925-- ack bookkeeping and SSRF-style filtering.
1972-- ack bookkeeping and SSRF-style filtering.
1926
1973
1927−-- POST /federation/verify: callback endpoint a peer invokes to validate a
1928−-- Bearer token we issued in `generateFederationAuth`. Single-use: a
1929−-- successful match removes the token from `pendingVerifications`.
1930−handleVerifyToken : Text -> Text -> Text -> ({ok: Bool} -> a) -> IO {reads *globalRateCount, reads *globalRateWindowStart, reads *ipRates, reads *pendingVerifications, writes *globalRateCount, writes *globalRateWindowStart, writes *ipRates, clock, writes *pendingVerifications} a
1931−handleVerifyToken = \token scope clientIp respond -> do
1932− t <- now
1933− g <- checkGlobalRate t
1934− ipLim <- checkIpRateGate clientIp t
1935− if scope == "" || g || ipLim
1936− then yield (respond {ok: False {}})
1937− else do
1938− found <- atomic (do
1939− pending <- *pendingVerifications
1940− let same = \p -> p.scope == scope && p.token == token
1941− let match = any pending (\p -> same p && t - p.createdAt < verificationTokenExpiry)
1942− when match (*pendingVerifications = removeWhere pending same)
1943− yield match)
1944− yield (respond {ok: found})
1945−
1946-- GET /federation/pubkey: publish the Ed25519 public key peers should use to
1974-- GET /federation/pubkey: publish the Ed25519 public key peers should use to
1947-- verify our `Signed ...` federation auth headers. Returns ok=False when the
1975-- verify our `Signed ...` federation auth headers. Returns ok=False when the
1948−-- server has no configured federation key (peers fall back to Bearer mode).
1976+-- keypair has not yet been generated (should not happen post-startup).
1949−handleFederationPubkey : ({pubkey: Text, ok: Bool} -> ret) -> IO {} ret
1977+handleFederationPubkey : ({pubkey: Text, ok: Bool} -> ret) -> IO {reads *federationPublicKeyHex} ret
1950handleFederationPubkey = \respond -> do
1978handleFederationPubkey = \respond -> do
1951− yield (respond {pubkey: serverFederationPublicKeyHex, ok: serverFederationPublicKeyHex != ""})
1979+ hex <- *federationPublicKeyHex
1980+ yield (respond {pubkey: hex, ok: hex != ""})
1952
1981
1953-- POST /federation/gossip: ingest peer presence events. Filters out events
1982-- POST /federation/gossip: ingest peer presence events. Filters out events
1954-- whose origin is local (loops) or whose timestamp is in the future beyond
1983-- whose origin is local (loops) or whose timestamp is in the future beyond
@@ -2205,7 +2234,7 @@ handleShutdown = \authorization respond -> handleDrain 2 authorization (\r -> re
2205-- POST /maintenance/prune: trigger an out-of-band `pruneAndRetry` tick.
2234-- POST /maintenance/prune: trigger an out-of-band `pruneAndRetry` tick.
2206-- Useful when the operator wants to force expiry/forward retries without
2235-- Useful when the operator wants to force expiry/forward retries without
2207-- waiting for the next `backgroundPrune` interval.
2236-- waiting for the next `backgroundPrune` interval.
2208−handlePrune : Text -> ({ok: Bool} -> a) -> IO {reads *runtimeMaintenanceSecret, clock, reads *ackedIds, reads *activePolls, reads *blockedPeers, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *forwardFailures, reads *forwards, reads *lastPresenceRefresh, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *pendingVerifications, reads *presence, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *lastPresenceRefresh, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *pendingVerifications, writes *presence, writes *senderDeliveries, writes *senderExpirations, writes *sessions} a
2237+handlePrune : Text -> ({ok: Bool} -> a) -> IO {reads *runtimeMaintenanceSecret, clock, reads *ackedIds, reads *activePolls, reads *blockedPeers, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *federationPrivateKeyHex, reads *forwardFailures, reads *forwards, reads *lastPresenceRefresh, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *presence, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *lastPresenceRefresh, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *presence, writes *senderDeliveries, writes *senderExpirations, writes *sessions} a
2209handlePrune = \authorization respond -> do
2238handlePrune = \authorization respond -> do
2210 t <- now
2239 t <- now
2211 authorized <- verifyMaintenanceAuth authorization
2240 authorized <- verifyMaintenanceAuth authorization
@@ -2325,7 +2354,7 @@ handleListAllowed = \authorization respond -> do
2325-- POST /maintenance/ping: send a zero-event gossip to a peer and return the
2354-- POST /maintenance/ping: send a zero-event gossip to a peer and return the
2326-- round-trip latency. Useful for diagnosing federation reachability without
2355-- round-trip latency. Useful for diagnosing federation reachability without
2327-- waiting for organic traffic.
2356-- waiting for organic traffic.
2328−handlePingPeer : Text -> Text -> ({error: Text, latencyMs: Int, ok: Bool} -> a) -> IO {reads *pendingVerifications, reads *runtimeMaintenanceSecret, writes *pendingVerifications, network, clock, random} a
2357+handlePingPeer : Text -> Text -> ({error: Text, latencyMs: Int, ok: Bool} -> a) -> IO {reads *federationPrivateKeyHex, reads *runtimeMaintenanceSecret, network, clock, random} a
2329handlePingPeer = \server authorization respond -> withMaintAuth authorization (yield (respond {
2358handlePingPeer = \server authorization respond -> withMaintAuth authorization (yield (respond {
2330 ok: False {},
2359 ok: False {},
2331 latencyMs: 0,
2360 latencyMs: 0,
@@ -2874,7 +2903,7 @@ handleMetrics = \authorization respond -> do
2874-- to the corresponding `handleXxx` function. Adding a new route means adding
2903-- to the corresponding `handleXxx` function. Adding a new route means adding
2875-- a `route ... = NewReq` line, a `handleNewReq` definition, and a clause here.
2904-- a `route ... = NewReq` line, a `handleNewReq` definition, and a clause here.
2876
2905
2877−serve : <AckMessages {authorization: Text, clientIp: Text, messageIds: [MessageIdHex], respond: {ackedCount: Int, authorized: Bool, error: Text, ok: Bool} -> Response} | AdminListAllSessions {authorization: Text, limit: Int, respond: {ok: Bool, sessions: [{count: Int, ips: [Text], latestExpiry: Timestamp, pubkey: PubkeyHex}], total: Int} -> Response} | AdminListSessions {authorization: Text, pubkey: PubkeyHex, respond: {count: Int, ok: Bool, sessions: [{createdAt: Timestamp, expiresAt: Timestamp, ip: Text, token: Text}]} -> Response} | AdminPurgeMessages {authorization: Text, pubkey: PubkeyHex, respond: {ok: Bool, purgedCount: Int} -> Response} | AdminRevokeIp {authorization: Text, ip: Text, respond: {affectedKeys: Int, ok: Bool, revokedCount: Int} -> Response} | AdminRevokeSessions {authorization: Text, pubkey: PubkeyHex, respond: {ok: Bool, revokedCount: Int} -> Response} | AllowPeer {authorization: Text, respond: {allowed: Int, ok: Bool} -> Response, server: ServerName} | BatchLookupPresence {authorization: Text, clientIp: Text, pubkeys: [PubkeyHex], respond: {authorized: Bool, error: Text, results: [{pubkey: PubkeyHex, server: AnyServerName}]} -> Response} | BlockPeer {authorization: Text, respond: {blocked: Int, ok: Bool} -> Response, server: ServerName} | CancelBatch {authorization: Text, cancels: [BatchCancelEntry], clientIp: Text, respond: {authorized: Bool, error: Text, results: [BatchCancelResult]} -> Response} | CancelMessage {authorization: Text, clientIp: Text, messageId: MessageIdHex, respond: {authorized: Bool, cancelled: Bool, error: Text, ok: Bool} -> Response, to: PubkeyHex} | DeleteMessages {authorization: Text, clientIp: Text, messageIds: [MessageIdHex], respond: {authorized: Bool, deletedCount: Int, error: Text, ok: Bool} -> Response} | Drain {authorization: Text, phase: Int, respond: {drained: Int, ok: Bool, phase: Int} -> Response} | FederationAck {authorization: Text, correlationId: Text, fromServer: ServerName, messageIds: [MessageIdHex], originServer: ServerName, protocolVersion: Int, requestTimestamp: Timestamp, respond: {ackedCount: Int, error: Text, ok: Bool, results: [{found: Bool, messageId: MessageIdHex}], retryAfterMs: Int} -> Response} | FederationBatchCancel {authorization: Text, cancels: [{messageId: MessageIdHex, toKey: PubkeyHex}], correlationId: Text, fromServer: ServerName, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, results: [BatchCancelResult], retryAfterMs: Int} -> Response} | FederationBatchForward {authorization: Text, correlationId: Text, forwards: [{encryptedBlob: BlobHex, expiresAt: Timestamp, messageId: MessageIdHex, originServer: ServerName, priority: Int, timestamp: Timestamp, toKey: PubkeyHex}], fromServer: ServerName, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} -> Response} | FederationCancel {authorization: Text, correlationId: Text, fromServer: ServerName, messageId: MessageIdHex, protocolVersion: Int, requestTimestamp: Timestamp, respond: {cancelled: Bool, error: Text, ok: Bool, retryAfterMs: Int} -> Response, toKey: PubkeyHex} | FederationDeliveryReceipt {authorization: Text, correlationId: Text, fromServer: ServerName, messageIds: [MessageIdHex], protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, receivedCount: Int, retryAfterMs: Int} -> Response, toKeys: [PubkeyHex]} | FederationPubkey {respond: {ok: Bool, pubkey: Text} -> Response} | FlushNonces {authorization: Text, respond: {flushedCount: Int, ok: Bool} -> Response} | ForwardMessage {authorization: Text, correlationId: Text, encryptedBlob: BlobHex, expiresAt: Timestamp, fromServer: ServerName, messageId: MessageIdHex, originServer: ServerName, priority: Int, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, retryAfterMs: Int} -> Response, timestamp: Timestamp, toKey: PubkeyHex} | GetConfig {authorization: Text, respond: {ackGracePeriod: Int, maxGossipHops: Int, maxMessagesPerRecipient: Int, ok: Bool, pollTimeoutMs: Int} -> Response} | GetRateLimits {authorization: Text, respond: {authGlobal: Int, challengeRate: Int, clientWriteRate: Int, federationRate: Int, globalRequest: Int, ipRate: Int, lookupRate: Int, ok: Bool, pairRate: Int, recvRate: Int, sendRate: Int} -> Response} | Health {authorization: Text, respond: {capacityHealthy: Bool, capacityWarning: Bool, degradedPeers: Int, draining: Bool, federationHealthy: Bool, forwardQueueHealthy: Bool, heartbeatHealthy: Bool, nonceHealthy: Bool, ok: Bool, pruneHealthy: Bool, requestPressureHealthy: Bool, version: Text, watchdogHealthy: Bool} -> Response} | ListAllowed {authorization: Text, respond: {ok: Bool, peers: [Text]} -> Response} | ListBlocked {authorization: Text, respond: {ok: Bool, peers: [Text]} -> Response} | ListMessages {authorization: Text, clientIp: Text, offset: Int, respond: {authorized: Bool, count: Int, error: Text, messages: [{messageId: MessageIdHex, receivedAt: Timestamp, timestamp: Timestamp}]} -> Response} | Logout {authorization: Text, clientIp: Text, respond: {authorized: Bool, ok: Bool} -> Response} | LookupPresence {authorization: Text, clientIp: Text, pubkey: PubkeyHex, respond: {authorized: Bool, error: Text, server: AnyServerName} -> Response} | MessageStatus {authorization: Text, clientIp: Text, messageId: MessageIdHex, respond: {authorized: Bool, status: Text} -> Response, to: PubkeyHex} | Metrics {authorization: Text, respond: {metrics: Text, ok: Bool} -> Response} | PingPeer {authorization: Text, respond: {error: Text, latencyMs: Int, ok: Bool} -> Response, server: ServerName} | Poll {authorization: Text, clientIp: Text, cursor: Timestamp, generation: Int, respond: {authorized: Bool, cursor: Timestamp, cursorReset: Bool, cursorStale: Bool, draining: Bool, droppedDeliveryReceipts: Int, droppedExpirationNotifs: Int, events: [PollEvent], generation: Int, hasMore: Bool, heartbeat: Bool, loadPct: Int, messageCancellations: [{cancelledAt: Timestamp, messageId: MessageIdHex}], messageDeliveries: [{deliveredAt: Timestamp, messageId: MessageIdHex, toKey: PubkeyHex}], messageExpirations: [{expiredAt: Timestamp, messageId: MessageIdHex, toKey: PubkeyHex}], presenceUpdates: [{online: Bool, pubkey: PubkeyHex}], rateLimited: Bool, retryAfterMs: Int} -> Response, watchKeys: [PubkeyHex]} | Prune {authorization: Text, respond: {ok: Bool} -> Response} | Ready {respond: {ready: Bool} -> Response} | Reconcile {authorization: Text, respond: {messageDrift: Int, ok: Bool, presenceDrift: Int, sessionDrift: Int} -> Response} | RecvGossip {authorization: Text, correlationId: Text, events: [GossipEvent], fromServer: ServerName, hopsRemaining: Int, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, retryAfterMs: Int} -> Response} | ReqChallenge {clientIp: Text, pubkey: PubkeyHex, respond: {challenge: Text, error: Text, expiresAt: Timestamp, ok: Bool, retryAfterMs: Int} -> Response} | RevokeSession {authorization: Text, clientIp: Text, respond: {authorized: Bool, ok: Bool} -> Response, targetToken: Text} | RotateSecret {authorization: Text, newSecret: Text, respond: {error: Text, ok: Bool} -> Response} | SendBatch {authorization: Text, clientIp: Text, messages: [BatchMessage], respond: {authorized: Bool, results: [BatchResult], retryAfterMs: Int} -> Response} | SendMessage {authorization: Text, clientIp: Text, encryptedBlob: BlobHex, expiresInMs: Int, messageId: OptionalMessageId, priority: Int, respond: {error: Text, loadPct: Int, messageId: OptionalMessageId, retryAfterMs: Int, status: MessageStatus} -> Response, timestamp: Timestamp, to: PubkeyHex} | SetRateLimits {authGlobal: Int, authorization: Text, challengeRate: Int, clientWriteRate: Int, federationRate: Int, globalRequest: Int, ipRate: Int, lookupRate: Int, pairRate: Int, recvRate: Int, respond: {ok: Bool} -> Response, sendRate: Int} | Shutdown {authorization: Text, respond: {drained: Int, ok: Bool} -> Response} | Stats {authorization: Text, respond: {capacity: {ackOverflow: Int, ackOverflowMax: Int, ackedIds: Int, ackedIdsMax: Int, activePolls: Int, activePollsMax: Int, challenges: Int, challengesMax: Int, deliveredIds: Int, deliveredIdsMax: Int, forwardNonces: Int, forwardNoncesMax: Int, forwards: Int, forwardsMax: Int, forwardsPct: Int, messageBucketMax: Int, messageBucketMin: Int, messages: Int, messagesMax: Int, messagesPct: Int, nonceEvictions: Int, presence: Int, presenceMax: Int, presencePct: Int, recipientCounts: Int, sessions: Int, sessionsMax: Int, sessionsPct: Int, totalMessageCount: Int}, federation: {avgLatencyMs: Int, blockedPeers: Int, callCount: Int, drainCompletedAt: Timestamp, enabled: Bool, forwardFailures: Int, forwardServers: Int, gossipFail: Int, gossipSuccess: Int, hungSlots: Int, inFlight: Int, latencyBuckets: {gte2000: Int, lt100: Int, lt2000: Int, lt500: Int}, maxForwardQueueDepth: Int, maxLatencyMs: Int, oldestAckAge: Int, oldestForwardAge: Int, pendingCancels: Int, recentGossip: Int, slowVerifies: Int, topFailingPeers: [{failures: Int, server: Text}], verifyFailures: Int}, ok: Bool, rates: {challenge: Int, clientCancels: Int, federation: Int, federationCancels: Int, federationPair: Int, global: Int, gossipEvent: Int, ip: Int, lookup: Int, pair: Int, pendingAckCleanups: Int, recv: Int, rejectedAuth: Int, rejectedFederation: Int, rejectedGlobal: Int, rejectedIp: Int, rejectedPerKey: Int, send: Int, senderDeliveries: Int, senderExpirations: Int, uniqueKeyIp: Int, verifyIp: Int}, timing: {activeRequests: Int, authAvgMs: Int, authCount: Int, authMaxMs: Int, drainPhase: Int, draining: Bool, heartbeatCount: Int, lastHeartbeatAt: Timestamp, lastPruneAt: Timestamp, peakActiveRequests: Int, pollAvgMs: Int, pollCount: Int, pollLatencyBuckets: {gte1000: Int, lt1000: Int, lt200: Int, lt50: Int}, pollMaxMs: Int, pruneCount: Int, pruneRunning: Timestamp, sendAvgMs: Int, sendCount: Int, sendLatencyBuckets: {gte1000: Int, lt1000: Int, lt200: Int, lt50: Int}, sendMaxMs: Int, totalEntries: Int, uptime: Int}, version: Text} -> Response} | StatsForwards {authorization: Text, respond: {ok: Bool, servers: [{count: Int, oldestAge: Int, server: Text, totalRetries: Int}], total: Int} -> Response} | StatsKey {authorization: Text, pubkey: PubkeyHex, respond: {activePolls: Int, forwardDetails: [{createdAt: Timestamp, retries: Int, server: Text}], forwards: Int, messages: Int, ok: Bool, presence: [KeyPresenceEntry], sessions: Int} -> Response} | StatsPeers {authorization: Text, respond: {ok: Bool, peers: [PeerStatus]} -> Response} | StatsRates {authorization: Text, respond: {authGlobal: Int, challengeRate: Int, clientWriteRate: Int, federationRate: Int, globalRequest: Int, ipRate: Int, lookupRate: Int, ok: Bool, pairRate: Int, recvRate: Int, scalePct: Int, sendRate: Int} -> Response} | TopRecipients {authorization: Text, limit: Int, respond: {ok: Bool, recipients: [RecipientCount], total: Int} -> Response} | UnallowPeer {authorization: Text, respond: {ok: Bool, removed: Bool} -> Response, server: ServerName} | UnblockPeer {authorization: Text, respond: {ok: Bool, unblocked: Bool} -> Response, server: ServerName} | Undrain {authorization: Text, respond: {ok: Bool} -> Response} | VerifyAuth {challenge: Text, clientIp: Text, pubkey: PubkeyHex, respond: {error: Text, expiresAt: Timestamp, token: Text} -> Response, revokeOthers: Bool, signature: SignatureHex} | VerifyToken {clientIp: Text, respond: {ok: Bool} -> Response, scope: Text, token: Text}> -> IO {reads *ackedIds, reads *activePolls, reads *allowedPeers, reads *authGlobalRateCount, reads *authGlobalRateWindowStart, reads *blockedPeers, reads *cancelNotifications, reads *challengeRates, reads *challenges, reads *deliveredIds, reads *drainCompletedAt, reads *drainPhase, reads *federationRates, reads *forwardFailures, reads *forwards, reads *globalRateCount, reads *globalRateWindowStart, reads *heartbeatCount, reads *ipRates, reads *lastHeartbeatAt, reads *lastPresenceRefresh, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *pendingVerifications, reads *pollWakeup, reads *presence, reads *pruneCount, reads *recvRates, reads *runtimeMaintenanceSecret, reads *senderDeliveries, reads *senderExpirations, reads *seqCounter, reads *serverGeneration, reads *sessions, reads *startedAt, writes *ackedIds, writes *activePolls, writes *allowedPeers, writes *authGlobalRateCount, writes *authGlobalRateWindowStart, writes *blockedPeers, writes *cancelNotifications, writes *challengeRates, writes *challenges, writes *deliveredIds, writes *drainCompletedAt, writes *drainPhase, writes *federationRates, writes *forwardFailures, writes *forwards, writes *globalRateCount, writes *globalRateWindowStart, writes *ipRates, writes *lastPresenceRefresh, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *pendingVerifications, writes *pollWakeup, writes *presence, writes *recvRates, writes *runtimeMaintenanceSecret, writes *senderDeliveries, writes *senderExpirations, writes *seqCounter, writes *sessions, network, clock, random} Response
2906+serve : <AckMessages {authorization: Text, clientIp: Text, messageIds: [MessageIdHex], respond: {ackedCount: Int, authorized: Bool, error: Text, ok: Bool} -> Response} | AdminListAllSessions {authorization: Text, limit: Int, respond: {ok: Bool, sessions: [{count: Int, ips: [Text], latestExpiry: Timestamp, pubkey: PubkeyHex}], total: Int} -> Response} | AdminListSessions {authorization: Text, pubkey: PubkeyHex, respond: {count: Int, ok: Bool, sessions: [{createdAt: Timestamp, expiresAt: Timestamp, ip: Text, token: Text}]} -> Response} | AdminPurgeMessages {authorization: Text, pubkey: PubkeyHex, respond: {ok: Bool, purgedCount: Int} -> Response} | AdminRevokeIp {authorization: Text, ip: Text, respond: {affectedKeys: Int, ok: Bool, revokedCount: Int} -> Response} | AdminRevokeSessions {authorization: Text, pubkey: PubkeyHex, respond: {ok: Bool, revokedCount: Int} -> Response} | AllowPeer {authorization: Text, respond: {allowed: Int, ok: Bool} -> Response, server: ServerName} | BatchLookupPresence {authorization: Text, clientIp: Text, pubkeys: [PubkeyHex], respond: {authorized: Bool, error: Text, results: [{pubkey: PubkeyHex, server: AnyServerName}]} -> Response} | BlockPeer {authorization: Text, respond: {blocked: Int, ok: Bool} -> Response, server: ServerName} | CancelBatch {authorization: Text, cancels: [BatchCancelEntry], clientIp: Text, respond: {authorized: Bool, error: Text, results: [BatchCancelResult]} -> Response} | CancelMessage {authorization: Text, clientIp: Text, messageId: MessageIdHex, respond: {authorized: Bool, cancelled: Bool, error: Text, ok: Bool} -> Response, to: PubkeyHex} | DeleteMessages {authorization: Text, clientIp: Text, messageIds: [MessageIdHex], respond: {authorized: Bool, deletedCount: Int, error: Text, ok: Bool} -> Response} | Drain {authorization: Text, phase: Int, respond: {drained: Int, ok: Bool, phase: Int} -> Response} | FederationAck {authorization: Text, correlationId: Text, fromServer: ServerName, messageIds: [MessageIdHex], originServer: ServerName, protocolVersion: Int, requestTimestamp: Timestamp, respond: {ackedCount: Int, error: Text, ok: Bool, results: [{found: Bool, messageId: MessageIdHex}], retryAfterMs: Int} -> Response} | FederationBatchCancel {authorization: Text, cancels: [{messageId: MessageIdHex, toKey: PubkeyHex}], correlationId: Text, fromServer: ServerName, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, results: [BatchCancelResult], retryAfterMs: Int} -> Response} | FederationBatchForward {authorization: Text, correlationId: Text, forwards: [{encryptedBlob: BlobHex, expiresAt: Timestamp, messageId: MessageIdHex, originServer: ServerName, priority: Int, timestamp: Timestamp, toKey: PubkeyHex}], fromServer: ServerName, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} -> Response} | FederationCancel {authorization: Text, correlationId: Text, fromServer: ServerName, messageId: MessageIdHex, protocolVersion: Int, requestTimestamp: Timestamp, respond: {cancelled: Bool, error: Text, ok: Bool, retryAfterMs: Int} -> Response, toKey: PubkeyHex} | FederationDeliveryReceipt {authorization: Text, correlationId: Text, fromServer: ServerName, messageIds: [MessageIdHex], protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, receivedCount: Int, retryAfterMs: Int} -> Response, toKeys: [PubkeyHex]} | FederationPubkey {respond: {ok: Bool, pubkey: Text} -> Response} | FlushNonces {authorization: Text, respond: {flushedCount: Int, ok: Bool} -> Response} | ForwardMessage {authorization: Text, correlationId: Text, encryptedBlob: BlobHex, expiresAt: Timestamp, fromServer: ServerName, messageId: MessageIdHex, originServer: ServerName, priority: Int, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, retryAfterMs: Int} -> Response, timestamp: Timestamp, toKey: PubkeyHex} | GetConfig {authorization: Text, respond: {ackGracePeriod: Int, maxGossipHops: Int, maxMessagesPerRecipient: Int, ok: Bool, pollTimeoutMs: Int} -> Response} | GetRateLimits {authorization: Text, respond: {authGlobal: Int, challengeRate: Int, clientWriteRate: Int, federationRate: Int, globalRequest: Int, ipRate: Int, lookupRate: Int, ok: Bool, pairRate: Int, recvRate: Int, sendRate: Int} -> Response} | Health {authorization: Text, respond: {capacityHealthy: Bool, capacityWarning: Bool, degradedPeers: Int, draining: Bool, federationHealthy: Bool, forwardQueueHealthy: Bool, heartbeatHealthy: Bool, nonceHealthy: Bool, ok: Bool, pruneHealthy: Bool, requestPressureHealthy: Bool, version: Text, watchdogHealthy: Bool} -> Response} | ListAllowed {authorization: Text, respond: {ok: Bool, peers: [Text]} -> Response} | ListBlocked {authorization: Text, respond: {ok: Bool, peers: [Text]} -> Response} | ListMessages {authorization: Text, clientIp: Text, offset: Int, respond: {authorized: Bool, count: Int, error: Text, messages: [{messageId: MessageIdHex, receivedAt: Timestamp, timestamp: Timestamp}]} -> Response} | Logout {authorization: Text, clientIp: Text, respond: {authorized: Bool, ok: Bool} -> Response} | LookupPresence {authorization: Text, clientIp: Text, pubkey: PubkeyHex, respond: {authorized: Bool, error: Text, server: AnyServerName} -> Response} | MessageStatus {authorization: Text, clientIp: Text, messageId: MessageIdHex, respond: {authorized: Bool, status: Text} -> Response, to: PubkeyHex} | Metrics {authorization: Text, respond: {metrics: Text, ok: Bool} -> Response} | PingPeer {authorization: Text, respond: {error: Text, latencyMs: Int, ok: Bool} -> Response, server: ServerName} | Poll {authorization: Text, clientIp: Text, cursor: Timestamp, generation: Int, respond: {authorized: Bool, cursor: Timestamp, cursorReset: Bool, cursorStale: Bool, draining: Bool, droppedDeliveryReceipts: Int, droppedExpirationNotifs: Int, events: [PollEvent], generation: Int, hasMore: Bool, heartbeat: Bool, loadPct: Int, messageCancellations: [{cancelledAt: Timestamp, messageId: MessageIdHex}], messageDeliveries: [{deliveredAt: Timestamp, messageId: MessageIdHex, toKey: PubkeyHex}], messageExpirations: [{expiredAt: Timestamp, messageId: MessageIdHex, toKey: PubkeyHex}], presenceUpdates: [{online: Bool, pubkey: PubkeyHex}], rateLimited: Bool, retryAfterMs: Int} -> Response, watchKeys: [PubkeyHex]} | Prune {authorization: Text, respond: {ok: Bool} -> Response} | Ready {respond: {ready: Bool} -> Response} | Reconcile {authorization: Text, respond: {messageDrift: Int, ok: Bool, presenceDrift: Int, sessionDrift: Int} -> Response} | RecvGossip {authorization: Text, correlationId: Text, events: [GossipEvent], fromServer: ServerName, hopsRemaining: Int, protocolVersion: Int, requestTimestamp: Timestamp, respond: {error: Text, ok: Bool, retryAfterMs: Int} -> Response} | ReqChallenge {clientIp: Text, pubkey: PubkeyHex, respond: {challenge: Text, error: Text, expiresAt: Timestamp, ok: Bool, retryAfterMs: Int} -> Response} | RevokeSession {authorization: Text, clientIp: Text, respond: {authorized: Bool, ok: Bool} -> Response, targetToken: Text} | RotateSecret {authorization: Text, newSecret: Text, respond: {error: Text, ok: Bool} -> Response} | SendBatch {authorization: Text, clientIp: Text, messages: [BatchMessage], respond: {authorized: Bool, results: [BatchResult], retryAfterMs: Int} -> Response} | SendMessage {authorization: Text, clientIp: Text, encryptedBlob: BlobHex, expiresInMs: Int, messageId: OptionalMessageId, priority: Int, respond: {error: Text, loadPct: Int, messageId: OptionalMessageId, retryAfterMs: Int, status: MessageStatus} -> Response, timestamp: Timestamp, to: PubkeyHex} | SetRateLimits {authGlobal: Int, authorization: Text, challengeRate: Int, clientWriteRate: Int, federationRate: Int, globalRequest: Int, ipRate: Int, lookupRate: Int, pairRate: Int, recvRate: Int, respond: {ok: Bool} -> Response, sendRate: Int} | Shutdown {authorization: Text, respond: {drained: Int, ok: Bool} -> Response} | Stats {authorization: Text, respond: {capacity: {ackOverflow: Int, ackOverflowMax: Int, ackedIds: Int, ackedIdsMax: Int, activePolls: Int, activePollsMax: Int, challenges: Int, challengesMax: Int, deliveredIds: Int, deliveredIdsMax: Int, forwardNonces: Int, forwardNoncesMax: Int, forwards: Int, forwardsMax: Int, forwardsPct: Int, messageBucketMax: Int, messageBucketMin: Int, messages: Int, messagesMax: Int, messagesPct: Int, nonceEvictions: Int, presence: Int, presenceMax: Int, presencePct: Int, recipientCounts: Int, sessions: Int, sessionsMax: Int, sessionsPct: Int, totalMessageCount: Int}, federation: {avgLatencyMs: Int, blockedPeers: Int, callCount: Int, drainCompletedAt: Timestamp, enabled: Bool, forwardFailures: Int, forwardServers: Int, gossipFail: Int, gossipSuccess: Int, hungSlots: Int, inFlight: Int, latencyBuckets: {gte2000: Int, lt100: Int, lt2000: Int, lt500: Int}, maxForwardQueueDepth: Int, maxLatencyMs: Int, oldestAckAge: Int, oldestForwardAge: Int, pendingCancels: Int, recentGossip: Int, slowVerifies: Int, topFailingPeers: [{failures: Int, server: Text}], verifyFailures: Int}, ok: Bool, rates: {challenge: Int, clientCancels: Int, federation: Int, federationCancels: Int, federationPair: Int, global: Int, gossipEvent: Int, ip: Int, lookup: Int, pair: Int, pendingAckCleanups: Int, recv: Int, rejectedAuth: Int, rejectedFederation: Int, rejectedGlobal: Int, rejectedIp: Int, rejectedPerKey: Int, send: Int, senderDeliveries: Int, senderExpirations: Int, uniqueKeyIp: Int, verifyIp: Int}, timing: {activeRequests: Int, authAvgMs: Int, authCount: Int, authMaxMs: Int, drainPhase: Int, draining: Bool, heartbeatCount: Int, lastHeartbeatAt: Timestamp, lastPruneAt: Timestamp, peakActiveRequests: Int, pollAvgMs: Int, pollCount: Int, pollLatencyBuckets: {gte1000: Int, lt1000: Int, lt200: Int, lt50: Int}, pollMaxMs: Int, pruneCount: Int, pruneRunning: Timestamp, sendAvgMs: Int, sendCount: Int, sendLatencyBuckets: {gte1000: Int, lt1000: Int, lt200: Int, lt50: Int}, sendMaxMs: Int, totalEntries: Int, uptime: Int}, version: Text} -> Response} | StatsForwards {authorization: Text, respond: {ok: Bool, servers: [{count: Int, oldestAge: Int, server: Text, totalRetries: Int}], total: Int} -> Response} | StatsKey {authorization: Text, pubkey: PubkeyHex, respond: {activePolls: Int, forwardDetails: [{createdAt: Timestamp, retries: Int, server: Text}], forwards: Int, messages: Int, ok: Bool, presence: [KeyPresenceEntry], sessions: Int} -> Response} | StatsPeers {authorization: Text, respond: {ok: Bool, peers: [PeerStatus]} -> Response} | StatsRates {authorization: Text, respond: {authGlobal: Int, challengeRate: Int, clientWriteRate: Int, federationRate: Int, globalRequest: Int, ipRate: Int, lookupRate: Int, ok: Bool, pairRate: Int, recvRate: Int, scalePct: Int, sendRate: Int} -> Response} | TopRecipients {authorization: Text, limit: Int, respond: {ok: Bool, recipients: [RecipientCount], total: Int} -> Response} | UnallowPeer {authorization: Text, respond: {ok: Bool, removed: Bool} -> Response, server: ServerName} | UnblockPeer {authorization: Text, respond: {ok: Bool, unblocked: Bool} -> Response, server: ServerName} | Undrain {authorization: Text, respond: {ok: Bool} -> Response} | VerifyAuth {challenge: Text, clientIp: Text, pubkey: PubkeyHex, respond: {error: Text, expiresAt: Timestamp, token: Text} -> Response, revokeOthers: Bool, signature: SignatureHex}> -> IO {reads *ackedIds, reads *activePolls, reads *allowedPeers, reads *authGlobalRateCount, reads *authGlobalRateWindowStart, reads *blockedPeers, reads *cancelNotifications, reads *challengeRates, reads *challenges, reads *deliveredIds, reads *drainCompletedAt, reads *drainPhase, reads *federationPrivateKeyHex, reads *federationPublicKeyHex, reads *federationRates, reads *forwardFailures, reads *forwards, reads *globalRateCount, reads *globalRateWindowStart, reads *heartbeatCount, reads *ipRates, reads *lastHeartbeatAt, reads *lastPresenceRefresh, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *pollWakeup, reads *presence, reads *pruneCount, reads *recvRates, reads *runtimeMaintenanceSecret, reads *senderDeliveries, reads *senderExpirations, reads *seqCounter, reads *serverGeneration, reads *sessions, reads *startedAt, writes *ackedIds, writes *activePolls, writes *allowedPeers, writes *authGlobalRateCount, writes *authGlobalRateWindowStart, writes *blockedPeers, writes *cancelNotifications, writes *challengeRates, writes *challenges, writes *deliveredIds, writes *drainCompletedAt, writes *drainPhase, writes *federationRates, writes *forwardFailures, writes *forwards, writes *globalRateCount, writes *globalRateWindowStart, writes *ipRates, writes *lastPresenceRefresh, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *pollWakeup, writes *presence, writes *recvRates, writes *runtimeMaintenanceSecret, writes *senderDeliveries, writes *senderExpirations, writes *seqCounter, writes *sessions, network, clock, random} Response
2878serve = \req -> case req of
2907serve = \req -> case req of
2879 ReqChallenge {pubkey, clientIp, respond} -> handleChallenge pubkey clientIp respond
2908 ReqChallenge {pubkey, clientIp, respond} -> handleChallenge pubkey clientIp respond
2880 VerifyAuth {pubkey, challenge, signature, revokeOthers, clientIp, respond} -> handleVerifyAuth pubkey challenge signature revokeOthers clientIp respond
2909 VerifyAuth {pubkey, challenge, signature, revokeOthers, clientIp, respond} -> handleVerifyAuth pubkey challenge signature revokeOthers clientIp respond
@@ -2898,7 +2927,6 @@ serve = \req -> case req of
2898 FederationBatchCancel {cancels, fromServer, requestTimestamp, authorization, respond} -> handleFederationBatchCancel cancels fromServer requestTimestamp authorization respond
2927 FederationBatchCancel {cancels, fromServer, requestTimestamp, authorization, respond} -> handleFederationBatchCancel cancels fromServer requestTimestamp authorization respond
2899 FederationBatchForward {forwards, fromServer, requestTimestamp, authorization, respond} -> handleFederationBatchForward forwards fromServer requestTimestamp authorization respond
2928 FederationBatchForward {forwards, fromServer, requestTimestamp, authorization, respond} -> handleFederationBatchForward forwards fromServer requestTimestamp authorization respond
2900 FederationDeliveryReceipt {messageIds, toKeys, fromServer, requestTimestamp, authorization, respond} -> handleFederationDeliveryReceipt messageIds toKeys fromServer requestTimestamp authorization respond
2929 FederationDeliveryReceipt {messageIds, toKeys, fromServer, requestTimestamp, authorization, respond} -> handleFederationDeliveryReceipt messageIds toKeys fromServer requestTimestamp authorization respond
2901− VerifyToken {token, scope, clientIp, respond} -> handleVerifyToken token scope clientIp respond
2902 FederationPubkey {respond} -> handleFederationPubkey respond
2930 FederationPubkey {respond} -> handleFederationPubkey respond
2903 Prune {authorization, respond} -> handlePrune authorization respond
2931 Prune {authorization, respond} -> handlePrune authorization respond
2904 Drain {phase, authorization, respond} -> handleDrain phase authorization respond
2932 Drain {phase, authorization, respond} -> handleDrain phase authorization respond
@@ -2940,7 +2968,7 @@ serve = \req -> case req of
2940-- `pollTimeoutMs` so long-poll holders unblock and return an empty heartbeat
2968-- `pollTimeoutMs` so long-poll holders unblock and return an empty heartbeat
2941-- response, keeping connections fresh.
2969-- response, keeping connections fresh.
2942
2970
2943−backgroundPrune : IO {reads *drainPhase, clock, reads *ackedIds, reads *activePolls, reads *blockedPeers, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *forwardFailures, reads *forwards, reads *lastPresenceRefresh, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *pendingVerifications, reads *presence, reads *pruneCount, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *lastPresenceRefresh, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *pendingVerifications, writes *presence, writes *pruneCount, writes *senderDeliveries, writes *senderExpirations, writes *sessions} a
2971+backgroundPrune : IO {reads *drainPhase, clock, reads *ackedIds, reads *activePolls, reads *blockedPeers, reads *cancelNotifications, reads *challenges, reads *deliveredIds, reads *federationPrivateKeyHex, reads *forwardFailures, reads *forwards, reads *lastPresenceRefresh, reads *lastPrunedAt, reads *messages, reads *pendingFederationCancels, reads *presence, reads *pruneCount, reads *senderDeliveries, reads *senderExpirations, reads *sessions, writes *ackedIds, writes *activePolls, writes *cancelNotifications, writes *challenges, writes *deliveredIds, writes *forwardFailures, writes *forwards, writes *lastPresenceRefresh, writes *lastPrunedAt, writes *messages, writes *pendingFederationCancels, writes *presence, writes *pruneCount, writes *senderDeliveries, writes *senderExpirations, writes *sessions} a
2944backgroundPrune = do
2972backgroundPrune = do
2945 sleep backgroundPruneInterval
2973 sleep backgroundPruneInterval
2946 dp <- *drainPhase
2974 dp <- *drainPhase
@@ -2969,6 +2997,13 @@ main = do
2969 t <- now
2997 t <- now
2970 genJitter <- randomInt 1000000
2998 genJitter <- randomInt 1000000
2971 atomic (do *startedAt = t; *serverGeneration = t * 1000 + genJitter)
2999 atomic (do *startedAt = t; *serverGeneration = t * 1000 + genJitter)
3000+ priv <- *federationPrivateKeyHex
3001+ when (priv == "") (do
3002+ let kp = generateKeyPair
3003+ atomic (do
3004+ *federationPrivateKeyHex = bytesToHex kp.privateKey
3005+ *federationPublicKeyHex = bytesToHex kp.publicKey)
3006+ logInfo "Generated federation keypair on first startup")
2972 logInfo ("Skrepka relay server starting on port " ++ show listenPort ++ "...")
3007 logInfo ("Skrepka relay server starting on port " ++ show listenPort ++ "...")
2973 logInfo (" federation: " ++ (if federationEnabled then "enabled" else "disabled"))
3008 logInfo (" federation: " ++ (if federationEnabled then "enabled" else "disabled"))
2974 fork backgroundPrune
3009 fork backgroundPrune