Repository navigation
Conversation
…he Users screen
When a customer was locked out by the email-code or password limits,
support had no way to see it or clear it short of running PHP on the
server. TieredRateLimiter::reset() existed, but nothing called it.
- RateLimit\Lockouts covers the email-keyed buckets (password_email,
magic_verify_email, magic_send_email, pw_reset_email, signup_email,
otp_send_recipient, password_otp_recipient). Each is keyed the way its
endpoint keys it, exact or by mailbox, and limits come from the
filtered policy. check() gives per-window counts and the lock time;
clear() resets every tier of one bucket or all of them.
- wp workos ratelimit check <email> lists them, one row per window with
count, limit, time left and locked. wp workos ratelimit clear <email>
[--bucket=<bucket>] clears one or all.
- WorkOS > Users gets a small "Sign-in lockouts" section below the user
list: email in, buckets out, Clear per bucket and Clear all. It calls
POST /workos/v1/admin/lockouts/{check,clear}, gated by edit_users. POST
keeps the address out of URLs.
- Every clear writes one rate_limit_cleared activity-log entry with the
user who cleared it, the source (cli or admin) and each bucket with its
subject hash, which matches that bucket's rate_limited entries. The
address isn't stored.
- Limiters can report a counter without charging it through a new
optional CountsAttempts interface, implemented by both shipped
limiters. The shared bucket constants on BaseEndpoint and
Signup::RATE_LIMIT_CREATE_EMAIL are now public so Lockouts uses the
same names and default.
Closes #61
bordoni
added this pull request to stack #82
October 17, 2026 18:47
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.
Stacked on #76 (
feat/rate-limit-policy-filter).When a customer gets locked out by the email-code or password limits, support has no way to see it or clear it without running PHP on the server.
TieredRateLimiter::reset()exists, but nothing called it.Changes
RateLimit\Lockoutscovers the buckets keyed on an email address:password_email,magic_verify_email,magic_send_email,pw_reset_email,signup_email,otp_send_recipientandpassword_otp_recipient. Each one is keyed the way its endpoint keys it (the exact address or the mailbox), so a lookup finds the counters the customer's requests hit. Limits come from the filtered policy.wp workos ratelimit check <email>lists those buckets, one row per window, with count, limit, time left and whether it's locked.--format=json|yaml|csvgives seconds.wp workos ratelimit clear <email> [--bucket=<bucket>]clears one bucket or all of them. It resets every window, so the next request goes through.edit_users.POST /workos/v1/admin/lockouts/checkand/clear, both gated byedit_users. They're POST so the address stays out of URLs and access logs.rate_limit_clearedactivity-log entry: the user who cleared it, the source (clioradmin) and each bucket with its subject hash. The hash matches that bucket'srate_limitedentries from Rate limits: make them filterable and log when they trigger #60. The address itself isn't stored.CountsAttemptsinterface. Both shipped limiters implement it. The shared bucket constants onBaseEndpointandSignup::RATE_LIMIT_CREATE_EMAILare now public, so the lookup uses the same names and default as the endpoints.docs/rate-limiting.mdandAGENTS.mdcover the command, the routes and the log entry.Screenshots
WorkOS Users screen, a locked address looked up:
Verified
LockoutsTest: locking an address the way the endpoints do, thencheckshows it (exact and mailbox keying), thenclearlets the next request through. Also covers clear all, every window, filtered limits, the log entry and bad input.LockoutsRestApiTest: signed-out, subscriber and editor callers get 401/403 on both routes and nothing is cleared. An admin can check and clear, and bad input gets a 400.RateLimitCommandTest:checkandclearagainst WP-CLI test doubles, since WP-CLI isn't in the test environment.RateLimiterTest:count()on both limiters.main(771 on Rate limits: make them filterable and log when they trigger #60), plus the 15constantstests.tscandbun run buildpass.Closes #61