crates/didbot-serve/src/replay.rs:22: oneREPLAY_CAPACITYof 100,000 entries is shared by every account. A full window refuses the next proof withReplayWindowFull(crates/didbot-serve/src/oauth/dpop.rs:704-712) rather than evicting.crates/didbot-serve/src/auth.rs:613: an entry is spent once the access token checks out, so the caller only needs one valid DPoP-bound grant.crates/didbot-serve/src/routes/session.rs:27(getSession) is one DPoP route that charges no per-account rate, so nothing slows the caller.- Failure: one account loops on
getSessionwith a freshjtieach time. AtDPOP_SKEW= 5 minutes (oauth/mod.rs:141) each entry lives 5 to 10 minutes, so about 170 to 330 requests a second fills the window. Every other account's DPoP-bound call, and every refresh at/oauth/token, then fails withReplayWindowFulluntil entries expire. Open PR https://tangled.org/permadeath.com/didbot/pulls/1211 shortens the hold to the nonce lifetime (two minutes at most), which raises the rate needed to about 830 a second but leaves the shared window.
Suggested fix: count live entries by the account the credential names (the client at /oauth/token). Refuse an account with ReplayWindowFull alone once it holds more than REPLAY_CAPACITY / N entries. Keep the global cap as a backstop. Only authenticated proofs reach accept, so every key is a real grant, and an unrated DPoP route then fills only the caller's own share.
Needs the owner's decision on N, which is a new parameter: it needs its own config key and documented range.
Source: https://tangled.org/permadeath.com/didbot/pulls/1211 ("Not built here, a per-account share of the replay window")
Filed by Claude (agent) from a static read of main at 0adf906a
Fixed in #1240. Each replay-window entry is charged to the account the credential names (the code or grant account at
/oauth/token, so one app serving many accounts is not one share). Past[oauth] dpop_proofs_per_account(default 2,000, range 100 to 100,000) that account alone getsReplayShareFull; the window-wide cap stays as a backstop.