Repository navigation
feat(api): make the per-IP invite limit configurable at runtime - #227
Conversation
The global ceiling and proof-of-work difficulty can already be changed with an environment variable, but the per-IP allowance is a constant, so raising it for a launch and putting it back means building and swapping a binary twice. Add INVITES_PER_IP_PER_DAY, defaulting to the existing 4. 0 is refused at startup because it would turn invites off, while 0 on the global ceiling turns the ceiling off. An IP can hold more than the limit after it is lowered, so retry_after now counts down to below the limit rather than to the oldest entry. Claude-Session: https://claude.ai/code/session_01VjxvYQUzmUE7o4fLzHUmpo
Cap the variable at 100, well under the global ceiling, and warn at startup when it differs from the default, so a forgotten raise shows up. with_max_per_window(0) now means 1 rather than refusing with no retry time. Write the rate-limit file to a temporary file and rename it. Restarts become routine with this setting, and a truncated file makes every invite return 500. Document the operating traps in the README: the build requirement, the verify step, that a bad value stops donations too, and that .env has no effect. Comments now say 4 is only the default. Tests tie the retry time to admission, cover the rejected and top values, clear the child's environment, and check create's refusal message. The startup test is renamed, since it reads the log and not a refused request. Claude-Session: https://claude.ai/code/session_01VjxvYQUzmUE7o4fLzHUmpo
|
Review for this PR (Full tier, because it touches abuse protection on a live service). Lenses run: code-first, skeptical, testing and big-picture, then code-first and skeptical again on the follow-up commits. All were independent Claude reviewers. External model: Codex was tried twice and failed both times with "You've hit your usage limit" (resets Nov 4). The big-picture lens ran in its place. Addressed: a lower upper bound (100, not 1000); a startup warning when the limit isn't the default; a zero guard; atomic saves of the limits file; a fuller README (build requirement, verify step, bad-value behaviour, dated comments, global ceiling, .env); stale comments; and more tests, including retry time against admission. Accepted without change:
[AI-assisted - Claude] |
Problem
The per-IP invite allowance is a compile-time constant.
GLOBAL_INVITES_PER_HOURandINVITE_POW_DIFFICULTYcan be changed by editing the service environment, but changing the per-IP limit means building and swapping a gkapi binary, and doing it again to put it back. People who share an address, such as phones behind a carrier NAT, reach this limit sooner than anyone else, so it needs to be adjustable around a traffic spike.Approach
Add
INVITES_PER_IP_PER_DAY(--invites-per-ip-per-day), wired the same way asGLOBAL_INVITES_PER_HOUR.test_rate_limiter_enforces_four_per_windowstill pins that default.retry_aftercounts down to below the limit. Lowering the limit after a raise leaves some IPs holding more than it, and the old calculation (oldest entry expires) would have told them to come back too early.The value itself is a production setting and is not changed here. The README gains an "Invite limits" section covering all three variables, the verify step after a restart, what a bad value does, and how to undo a change.
Testing
tests/invite_limit_env.rsstarts the real binary with a cleared environment. It checks the startup log for a set value, the top value and the default, and that0,101,abcand a blank value are refused. It reads the log, not a refused request; the log reports the limiter's own value, and the unit tests take over from there.configured_per_ip_limit_is_enforced_and_reportedcovers both handlers and their refusal messages.retry_after_counts_down_to_below_the_limit_when_over_itandadmission_reopens_when_the_retry_time_says_it_doescover the rollback case, and that the retry time matches what admission does.failed_save_leaves_the_existing_file_wholecovers the atomic write.cargo test -p ghostkey-apipasses locally.This is abuse-protection code, so it needs a Full-tier review before merge. Merging ships nothing: gkapi is deployed by hand (see
rust/api/README.md).[AI-assisted - Claude]
https://claude.ai/code/session_01VjxvYQUzmUE7o4fLzHUmpo