on master help sign in
Remove function parameters from capList and capPerKey; unify on expiresAt field
commit58f6e236eaec0f209308fbda51b87a31d26e5669 authoriko <mail@iko.soy> dateSat 9 May 2026, 21:32 (5 months ago) parentd786f53
1 file changed+20−20
server.knot+20−20
server.knot+20−20
@@ -225,10 +225,10 @@ type ForwardEntry = {toServer: ServerName, toKey: PubkeyHex, encryptedBlob: Blob
225225-- Running failure counter for a peer; keyed by `server`. Drives `retryBackoff`.
226226type ForwardFailure = {server: Text, failedAt: Timestamp, failures: Int, firstFailedAt: Timestamp}
227227-- One fixed-window rate counter. `key` identifies the subject (pubkey, ip,
228−-- server, or composite "sender|recipient" string). `windowStart` is the wall
229−-- time at which the current window began; once `t - windowStart` exceeds the
230−-- configured window length the entry is recycled (counter resets to 1).
231−type RateEntry = {key: Text, windowStart: Timestamp, count: Int}
228+-- server, or composite "sender|recipient" string). `expiresAt` is the wall
229+-- time at which the current window ends; once `t >= expiresAt` the entry is
230+-- recycled (counter resets to 1).
231+type RateEntry = {key: Text, expiresAt: Timestamp, count: Int}
232232
233233-- ============================================
234234-- Persisted State (all flat lists, no sharding)
@@ -311,18 +311,17 @@ route Api = AuthApi | ClientApi | FederationApi
311311clamp : Ord a => a -> a -> a -> a
312312clamp = \lo hi x -> max lo (min hi x)
313313
314−-- Cap to maxN by dropping the smallest-key entries (LRU-style). Always sorts.
315−-- For session/challenge/rate caps, `getKey` is typically `expiresAt` or
316−-- `windowStart` so the oldest entries are evicted first.
317−capList : [a] -> Int -> (a -> Timestamp) -> [a]
318−capList = \items maxN getKey -> drop (max 0 (count items - maxN)) (sortBy getKey items)
314+-- Cap to maxN by dropping the soonest-to-expire entries (LRU-style). Always
315+-- sorts by `expiresAt` so the oldest/most-stale entries are evicted first.
316+capList : [{expiresAt: Timestamp | r}] -> Int -> [{expiresAt: Timestamp | r}]
317+capList = \items maxN -> drop (max 0 (count items - maxN)) (sortBy (\x -> x.expiresAt) items)
319318
320319-- Cap entries matching `pubkey` to maxN-1 (caller adds one more). Splits the
321320-- list into "different pubkey" (untouched) and "same pubkey" (capped +
322321-- LRU-evicted) so a single key can't exhaust a global pool — see
323322-- `maxChallengesPerKey` and `maxSessionsPerKey`.
324−capPerKey : [{pubkey: PubkeyHex | r}] -> PubkeyHex -> Int -> ({pubkey: PubkeyHex | r} -> Timestamp) -> [{pubkey: PubkeyHex | r}]
325−capPerKey = \items pubkey maxN getKey -> union (filter (\x -> not (x.pubkey == pubkey)) items) (capList (filter (\x -> x.pubkey == pubkey) items) (maxN - 1) getKey)
323+capPerKey : [{pubkey: PubkeyHex, expiresAt: Timestamp | r}] -> PubkeyHex -> Int -> [{pubkey: PubkeyHex, expiresAt: Timestamp | r}]
324+capPerKey = \items pubkey maxN -> union (filter (\x -> not (x.pubkey == pubkey)) items) (capList (filter (\x -> x.pubkey == pubkey) items) (maxN - 1))
326325
327326-- Sequence an IO action over each item, in order. Recurses through the list
328327-- one element at a time; this is the closest equivalent of `mapM_` here.
@@ -470,13 +469,14 @@ withSessionAuth = \authorization t clientIp onUnauth onAuth -> do
470469-- gate for unauthenticated handlers (challenge/verify).
471470
472471-- Returns {limited, entries}. Increments count when the key fits within the
473−-- window; otherwise resets to a fresh window starting at `t`. The returned
474−-- `entries` list is what the caller must write back to the corresponding ref;
475−-- it has been capped at 5000 rows (LRU by `windowStart`) to bound memory.
472+-- window; otherwise resets to a fresh window expiring at `t + windowMs`. The
473+-- returned `entries` list is what the caller must write back to the
474+-- corresponding ref; it has been capped at 5000 rows (LRU by `expiresAt`) to
475+-- bound memory.
476476checkRate : [RateEntry] -> Text -> Int -> Int<Ms> -> Timestamp -> {limited: Bool, entries: [RateEntry]}
477−checkRate = \entries key maxRate windowMs t -> (\cur -> if cur.count > maxRate then {limited: True {}, entries} else {limited: False {}, entries: capList (union (filter (\x -> not (x.key == key)) entries) [{key, cur.windowStart, cur.count}]) 5000 (\r -> r.windowStart)}) (case findFirst entries (\r -> r.key == key && t - r.windowStart < windowMs) of
478− Just {value: r} -> {r.windowStart, count: r.count + 1}
479− _ -> {windowStart: t, count: 1})
477+checkRate = \entries key maxRate windowMs t -> (\cur -> if cur.count > maxRate then {limited: True {}, entries} else {limited: False {}, entries: capList (union (filter (\x -> not (x.key == key)) entries) [{key, cur.expiresAt, cur.count}]) 5000}) (case findFirst entries (\r -> r.key == key && r.expiresAt > t) of
478+ Just {value: r} -> {r.expiresAt, count: r.count + 1}
479+ _ -> {expiresAt: t + windowMs, count: 1})
480480
481481-- Single-counter global rate gate. Resets the counter when the window expires;
482482-- returns True when the limit has been hit (caller responds rate_limited).
@@ -735,7 +735,7 @@ handleChallenge = \pubkey clientIp -> do
735735 challenge <- genToken
736736 result <- atomic (do
737737 ch <- *challenges
738− let cappedActive = capPerKey (filter (\c -> c.expiresAt > t) ch) pubkey maxChallengesPerKey (\c -> c.expiresAt)
738+ let cappedActive = capPerKey (filter (\c -> c.expiresAt > t) ch) pubkey maxChallengesPerKey
739739 if count cappedActive >= maxChallenges
740740 then yield "capacity"
741741 else do
@@ -782,9 +782,9 @@ handleVerifyAuth = \pubkey challenge signature revokeOthers clientIp -> do
782782 ses <- *sessions
783783 let pruned = if revokeOthers
784784 then filter (\s -> not (s.pubkey == pubkey)) ses
785− else capPerKey ses pubkey maxSessionsPerKey (\s -> s.expiresAt)
785+ else capPerKey ses pubkey maxSessionsPerKey
786786 let newSession = {token, pubkey, expiresAt: expiry, ip: clientIp}
787− *sessions = capList (union pruned [newSession]) maxSessions (\s -> s.expiresAt)
787+ *sessions = capList (union pruned [newSession]) maxSessions
788788 pres <- *presence
789789 let prunedPres = filter (\p -> not (p.pubkey == pubkey && isLocalServer p.server)) pres
790790 let newPres = union prunedPres [{pubkey, server: serverHost, expiresAt: expiry}]