Skip to content

feat: resetAll() — bulk administrative unlock#14

Merged
rodrigobnogueira merged 1 commit into
mainfrom
feat/reset-all
Jul 20, 2026
Merged

feat: resetAll() — bulk administrative unlock#14
rodrigobnogueira merged 1 commit into
mainfrom
feat/reset-all

Conversation

@rodrigobnogueira

Copy link
Copy Markdown
Contributor

What

A bulk "unlock everyone" for incident response — LockoutManager.resetAll() / LockoutService.resetAll(), backed by a new clearAll() on the LockoutStore seam.

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.
  • Per-identity reset across all IPs already works for single-dimension parameters: reset({ username }) clears that username's lock regardless of IP (the ['ip'] parameter is skipped when no IP is given). Now documented.
  • Combination-key partial reset ([['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 use resetAll() for that case.

How / tests

  • clearAll() on the interface + in-memory + all three Drizzle stores (MySQL keeps its affectedRows shape).
  • Contract test runs across in-memory + pglite + sqlite, and — ran — real Postgres + MySQL (infra:up && test:full, 12/12).
  • resetAll() fails open on store error (logs, doesn't throw); adapter passthrough test.
  • 100% core coverage, complexity ≤15, core typecheck clean, website builds.

Additive + backward compatible. No version bump here — CHANGELOG Unreleased accumulates until the 0.4.0 release.

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.
@github-actions

Copy link
Copy Markdown

📊 Coverage Coverage Report

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

@github-actions

Copy link
Copy Markdown

⏱️ Performance Report

✅ Tests 99 passed, 0 failed, 0 skipped
🧪 Suites 22
⏱️ Total step time 26.00s ⚪ ~0
⚙️ Test execution 20.96s 🔴 +529ms
🐢 Slowest test suites
# Suite Tests Duration vs Base
1 LockoutStore contract — postgres (pglite) 6 11.37s 🔴 +959ms
2 LockoutStore contract — sqlite (better-sqlite3) 6 24ms ⚪ ~0
3 MysqlLockoutStore (real drizzle-mysql2 builder, fake client) 6 10ms ⚪ ~0
4 @authlock/core acceptance (framework-neutral login flow) 2 4ms ⚪ ~0
5 lockout table schema parity 4 2ms ⚪ ~0
6 deriveKeys 13 2ms ⚪ ~0
7 LockoutManager — configuration validation 8 2ms ⚪ ~0
8 LockoutStore contract — in-memory 6 1ms ⚪ ~0
9 InMemoryLockoutStore 6 1ms ⚪ ~0
10 LockoutManager — basic lifecycle 4 1ms ⚪ ~0
🐌 Slowest individual tests
# Suite Test Duration vs Base
1 LockoutStore contract — postgres (pglite) LockoutStore contract — postgres (pglite) creates, increments (advancing lastFailureAt), then resets after the window 2.44s 🟢 -125ms
2 LockoutStore contract — postgres (pglite) LockoutStore contract — postgres (pglite) clearExpired removes only records older than the cutoff and counts them 1.83s 🟢 -122ms
3 LockoutStore contract — postgres (pglite) LockoutStore contract — postgres (pglite) counts concurrent increments exactly — no lost updates 1.81s 🟢 -184ms
4 LockoutStore contract — postgres (pglite) LockoutStore contract — postgres (pglite) clears a single key 1.77s 🟢 -188ms
5 LockoutStore contract — postgres (pglite) LockoutStore contract — postgres (pglite) reads a stored record, or null when the key is unknown 1.77s 🟢 -180ms
6 LockoutStore contract — postgres (pglite) LockoutStore contract — postgres (pglite) clearAll removes every key regardless of age 1.76s -
7 LockoutStore contract — sqlite (better-sqlite3) LockoutStore contract — sqlite (better-sqlite3) counts concurrent increments exactly — no lost updates 11ms ⚪ ~0
8 MysqlLockoutStore (real drizzle-mysql2 builder, fake client) MysqlLockoutStore (real drizzle-mysql2 builder, fake client) increment issues an atomic windowed upsert then re-reads the row 7ms ⚪ ~0
9 LockoutStore contract — sqlite (better-sqlite3) LockoutStore contract — sqlite (better-sqlite3) creates, increments (advancing lastFailureAt), then resets after the window 6ms ⚪ ~0
10 @authlock/core acceptance (framework-neutral login flow) @authlock/core acceptance (framework-neutral login flow) locks a brute-force loop after the limit, then recovers after cooloff 2ms ⚪ ~0
11 LockoutStore contract — sqlite (better-sqlite3) LockoutStore contract — sqlite (better-sqlite3) reads a stored record, or null when the key is unknown 2ms ⚪ ~0
12 LockoutStore contract — sqlite (better-sqlite3) LockoutStore contract — sqlite (better-sqlite3) clearAll removes every key regardless of age 2ms -
13 LockoutStore contract — sqlite (better-sqlite3) LockoutStore contract — sqlite (better-sqlite3) clearExpired removes only records older than the cutoff and counts them 2ms ⚪ ~0
14 @authlock/core acceptance (framework-neutral login flow) @authlock/core acceptance (framework-neutral login flow) exposes a semver-shaped version 1ms ⚪ ~0
15 MysqlLockoutStore (real drizzle-mysql2 builder, fake client) MysqlLockoutStore (real drizzle-mysql2 builder, fake client) get returns the mapped record when present 1ms ⚪ ~0

Updated for bbf6847 | Compared against base branch

@github-actions

Copy link
Copy Markdown

🧠 Cognitive Complexity Report

Metric PR Base Diff
Total complexity 72 71 🔴 +1
Max function complexity 11 11 ⚪ 0
Functions measured 27 26 ⚪ +1
🧩 Most complex functions
Function Location Complexity Status
deriveKeys packages/core/key.ts:32 11 🟢 OK
validateTiers packages/core/manager.ts:28 11 🟢 OK
validateNormalize packages/core/manager.ts:72 4 🟢 OK
cooloffFor packages/core/policy.ts:10 4 🟢 OK
evaluateRecord packages/core/policy.ts:78 4 🟢 OK
mostRestrictive packages/core/manager.ts:105 3 🟢 OK
LockoutManager.clearKeys packages/core/manager.ts:259 3 🟢 OK
LockoutManager.applyFailure packages/core/manager.ts:280 3 🟢 OK
effectiveWindowMs packages/core/policy.ts:33 3 🟢 OK
InMemoryLockoutStore.clearExpired packages/core/store/in-memory.ts:49 3 🟢 OK
🧾 Changed files
File Total Max Functions Total diff
core/dialects/mysql/lockout-store.ts 1 1 1 ⚪ 0
core/dialects/postgres/lockout-store.ts 1 1 1 ⚪ 0
core/dialects/sqlite/lockout-store.ts 1 1 1 ⚪ 0
core/manager.ts 43 11 18 🔴 +1
core/store/in-memory.ts 4 3 2 ⚪ 0

🧭 Cognitive complexity is reported as a review signal, not a merge gate. Prefer small, intention-revealing refactors when complexity rises.


Updated for bbf6847 | Compared against base branch

@rodrigobnogueira
rodrigobnogueira merged commit 0c2da4d into main Jul 20, 2026
11 checks passed
@rodrigobnogueira
rodrigobnogueira deleted the feat/reset-all branch July 20, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant