Feature set 130. - #57
Merged
Merged
Conversation
Enforce the premium rate limit against an allowlist. The X-Api-Key header was taken at face value: any non-empty value bought 600 req/min instead of 60, because the value was never compared against anything. Worse, the limiter partitioned on the raw header, so varying it minted unlimited fresh windows -- the limit was not 10x too high, it was effectively absent for anyone who noticed. Keys now come from an allowlist (ApiKeys__Keys__0, ...). An unrecognized key falls back to the anonymous limit rather than 401, keeping the documented "no API key required" contract intact. Also removed the two named rate limit policies. No endpoint ever attached them with RequireRateLimiting, so they enforced nothing while appearing to, and the premium one carried the same flaw waiting to be wired up. Only GlobalLimiter was ever live. The request log had the same defect: IsPremium counted anyone who sent the header, valid or not, inflating the premium share in /api/stats. It now records recognized keys, captured while the request is still live rather than resolved from a disposed scope inside the fire-and-forget write. Separately, the dashboard's monster search now matches description as well as name, mirroring /api/monsters/search. Auditing the catalogue turned up ~3,200 enemy-ability pages and ~130 enemy-type pages that the wiki scrape filed as monsters, and what marks them -- "#REDIRECT ... enemy abilities", "may refer to", "is a genus of" -- lives only in the description. Searching names alone left them unreachable from the dashboard that curates them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Enforce the premium rate limit against an allowlist
X-Api-Keywas taken at face value — any non-empty value bought 600 req/min instead of 60, because the value was never compared against anything:The second line is the worse half. Partitioning on the raw header means varying it mints unlimited fresh windows, so the limit wasn't 10× too high — it was effectively absent for anyone who noticed.
Keys now come from an allowlist (
ApiKeys__Keys__0, …), so a partition is only ever keyed on a credential we issued. An unrecognized key falls back to the anonymous limit rather than401, keeping the documented "no API key required" contract intact. With no keys configured, everything is anonymous.Also in here
The two named rate-limit policies are gone. No endpoint ever attached them with
RequireRateLimiting, so they enforced nothing while appearing to — and the premium one carried the identical flaw, waiting for whoever wired it up. OnlyGlobalLimiterwas ever live.The request log had the same defect.
IsPremiumcounted anyone who sent the header, valid or not, inflating the premium share in/api/stats. It now records recognized keys, and captures the value while the request is still live instead of resolving from a scope that the fire-and-forget write may find disposed.Dashboard monster search now matches description as well as name, mirroring
/api/monsters/search. Auditing the catalogue turned up 3,218 enemy-ability pages and 128 enemy-type pages that the wiki scrape filed as monsters — 47.5% of the 7,062 rows. What marks them (#REDIRECT … enemy abilities,may refer to,is a genus of) lives only in the description, so searching names alone left them unreachable from the dashboard that curates them.Testing
ApiKeyTestscovers the regression directly — 14 cases over unrecognized keys, case sensitivity, prefix matching, blank config values binding from unset env vars, and the normalization that stops one key holding several rate-limit windows.324/324 pass;
dotnet formatclean.Notes
HashSetlookup, not constant-time. Proportionate for something granting a rate-limit tier rather than data access, but it is a bearer credential.%term%scan with no index — fine at 7k rows on an admin page. It also means searching a common word returns monsters that merely mention it.release:minor: new configuration and changed documented behaviour, but no break to the public contract, since a key was never required.🤖 Generated with Claude Code