feat: resetAll() — bulk administrative unlock#14
Merged
Conversation
An "unlock everyone" for incident response (a false-positive lockout wave, a
migration): clears every counter at once, distinct from the window-based
pruneExpired().
- interfaces.ts: `clearAll()` on the LockoutStore seam.
- stores: implemented in the in-memory store (`records.clear()`) and all three
Drizzle stores (`delete(table)` with no WHERE; MySQL keeps its affectedRows
shape). Contract test in store-contract.ts runs against in-memory + pglite +
sqlite, and — verified — real Postgres and MySQL via test:full.
- manager.ts: `resetAll()` beside `reset()`, fail-open on store error (logs).
- adapter: LockoutService.resetAll() passthrough.
- Docs: api-reference rows + a "Bulk unlock" note that also documents an
HONEST limitation — single-dimension `reset({username})` already unlocks
across all IPs, but per-identity unlock across a COMBINATION parameter is not
supported (the hashed key can't be reversed to a dimension without a
reverse index that would defeat "credentials never reach the store").
Additive and backward compatible. 100% core coverage; complexity <=15.
📊
|
| Metric | PR | Base | Diff | |
|---|---|---|---|---|
| Statements | ████████████████████ |
959/959 (100%) |
927/927 (100%) |
⚪ 0% |
| Branches | ████████████████████ |
165/165 (100%) |
159/159 (100%) |
⚪ 0% |
| Functions | ████████████████████ |
56/56 (100%) |
51/51 (100%) |
⚪ 0% |
| Lines | ████████████████████ |
959/959 (100%) |
927/927 (100%) |
⚪ 0% |
🧾 Changed files
| File | Statements | Branches | Diff | |
|---|---|---|---|---|
core/dialects/mysql/lockout-store.ts |
████████████████████ |
78/78 (100%) |
8/8 (100%) |
⚪ 0% |
core/dialects/postgres/lockout-store.ts |
████████████████████ |
73/73 (100%) |
8/8 (100%) |
⚪ 0% |
core/dialects/sqlite/lockout-store.ts |
████████████████████ |
73/73 (100%) |
8/8 (100%) |
⚪ 0% |
core/manager.ts |
████████████████████ |
372/372 (100%) |
94/94 (100%) |
⚪ 0% |
core/store/in-memory.ts |
████████████████████ |
59/59 (100%) |
12/12 (100%) |
⚪ 0% |
Updated for bbf6847 | Compared against base branch
⏱️ Performance Report
🐢 Slowest test suites
🐌 Slowest individual tests
Updated for |
🧠 Cognitive Complexity Report
🧩 Most complex functions
🧾 Changed files
Updated for |
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.
What
A bulk "unlock everyone" for incident response —
LockoutManager.resetAll()/LockoutService.resetAll(), backed by a newclearAll()on theLockoutStoreseam.Honest scope (the ops-reset evaluation)
The ask was "config-gated ops reset, default = current behavior." Grounded in the code, the honest shape is:
resetAll()is the genuine additive win (nothing could clear the whole store before). It needs no config flag — it's purely additive, so the default is the current behavior unless you deliberately call it.reset({ username })clears that username's lock regardless of IP (the['ip']parameter is skipped when no IP is given). Now documented.[['username','ip']], "unlock username X across every IP") is not added: the store is keyed by a one-way hash, so serving it needs a reverse index of raw dimensions — which defeats the library's "credentials never reach the store" invariant. Documented as the reason to useresetAll()for that case.How / tests
clearAll()on the interface + in-memory + all three Drizzle stores (MySQL keeps itsaffectedRowsshape).infra:up && test:full, 12/12).resetAll()fails open on store error (logs, doesn't throw); adapter passthrough test.Additive + backward compatible. No version bump here — CHANGELOG
Unreleasedaccumulates until the 0.4.0 release.