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