@@ -314,15 +314,15 @@ clamp = \lo hi x -> max lo (min hi x)
314314-- Cap to maxN by dropping the smallest-key entries (LRU-style). Always sorts.
315315-- For session/challenge/rate caps, `getKey` is typically `expiresAt` or
316316-- `windowStart` so the oldest entries are evicted first.
317−capList : [a] -> Int -> (a -> b) -> [a]
317+capList : [a] -> Int -> (a -> Timestamp) -> [a]
318318capList = \items maxN getKey -> drop (max 0 (count items - maxN)) (sortBy getKey items)
319319
320−-- Cap pred-matching entries to maxN-1 (caller adds one more). Splits the list
321−-- into "doesn't match" (untouched) and "matches" (capped + LRU-evicted) so a
322−-- single key can't exhaust a global pool — see `maxChallengesPerKey` and
323−-- `maxSessionsPerKey`.
324−capPerKey : [a] -> (a -> Bool) -> Int -> (a -> b) -> [a]
325−capPerKey = \items pred maxN getKey -> union (filter (\x -> not pred x) items) (capList (filter pred items) (maxN - 1) getKey)
320+-- Cap entries matching `pubkey` to maxN-1 (caller adds one more). Splits the
321+-- list into "different pubkey" (untouched) and "same pubkey" (capped +
322+-- LRU-evicted) so a single key can't exhaust a global pool — see
323+-- `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)
326326
327327-- Sequence an IO action over each item, in order. Recurses through the list
328328-- one element at a time; this is the closest equivalent of `mapM_` here.
@@ -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) (\c -> c.pubkey == pubkey) maxChallengesPerKey (\c -> c.expiresAt)
738+ let cappedActive = capPerKey (filter (\c -> c.expiresAt > t) ch) pubkey maxChallengesPerKey (\c -> c.expiresAt)
739739 if count cappedActive >= maxChallenges
740740 then yield "capacity"
741741 else do
@@ -780,10 +780,9 @@ handleVerifyAuth = \pubkey challenge signature revokeOthers clientIp -> do
780780 else do
781781 *challenges = filter (\c -> not same c) ch
782782 ses <- *sessions
783− let mine = \s -> s.pubkey == pubkey
784783 let pruned = if revokeOthers
785− then filter (\s -> not mine s) ses
786− else capPerKey ses mine maxSessionsPerKey (\s -> s.expiresAt)
784+ then filter (\s -> not (s.pubkey == pubkey)) ses
785+ else capPerKey ses pubkey maxSessionsPerKey (\s -> s.expiresAt)
787786 let newSession = {token, pubkey, expiresAt: expiry, ip: clientIp}
788787 *sessions = capList (union pruned [newSession]) maxSessions (\s -> s.expiresAt)
789788 pres <- *presence