The DPoP replay-window share is now held per DPoP key rather than per account. A sign-in binds its tokens to one key, so each app's sign-in has its own share: an app looping getSession on an atproto-only login fills only its own, and the account's other apps keep writing, refreshing and exchanging codes. A key at its share is still refused alone, with the same default of 2,000 live proofs. The config key keeps its name, [oauth] dpop_proofs_per_account, so existing configs load unchanged; its docs now say it bounds one key.
POST /oauth/token now checks the key's share before it redeems an authorization code. A refused proof no longer spends the code, so the agent's approval is still there to retry with.
The new test fails on main twice over: with the per-account share, app B's write is refused once app A loops getSession; with the share check after redemption, the refused exchange spends the code. No deploy window is needed: this is an ordinary release with no data layout or config change, and rolling back is safe.
Closes https://tangled.org/permadeath.com/didbot/issues/241
🤖 Generated with Claude Code