Repository navigation
fix(plugin-auth): the account lockout resolves the account the sign-in credential check resolves - #24843
Conversation
…tial check resolves ADR-0069 D2's lock gate, its counter and the account events now share one resolver, findSignInAccount, which asks sys_user exactly what better-auth's /sign-in/email credential check asks. Every refused sign-in that resolves to an account counts toward that account's one lock, an existing lock refuses every sign-in that resolves to the account, and a success under any spelling clears the counter. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…check resolves The authentication guide states which account the password-stage counter and lock are held on, and the changeset records the fix for plugin-auth. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…resolver removed The lock gate and the counter no longer cast their sys_user query to any, so the query-options-erasure ratchet for auth-manager.ts moves down from 14 to 12. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0f76135c2ab89fad249d66a82da042ee5099a6be && git checkout 0f76135c2ab89fad249d66a82da042ee5099a6be
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 99e4eaa227e0375b59c7bbdf6e36ad7f026bb22d 94ea60bd5fa06a72e5f6334b9900968d34b371ba && git checkout -B drift-repro 99e4eaa227e0375b59c7bbdf6e36ad7f026bb22d && git merge --no-ff 94ea60bd5fa06a72e5f6334b9900968d34b371ba
node scripts/docs-audit/affected-docs.mjs --json 99e4eaa227e0375b59c7bbdf6e36ad7f026bb22d
|
席位核对ACCEPT · PR #24843 at
Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge. This repo's merge queue rebuilds on the current |
|
Landed ·
State: there is no card to close. This was the maintainer's direct dispatch, with no public card by their ruling. The fix ships in the next |
Security fix (maintainer-ordered; no public card; detail withheld under the disclosure ruling).
Clause-②: no
The invariant this restores
ADR-0069 D2's account lockout counts every refused password sign-in against the account the credential check resolved, and enforces an existing lock on every sign-in attempt that resolves to that account, whatever spelling of the identifier the attempt used. The lock gate, the failed sign-in counter, the credential check and the
login_failed/account_lockedaccount events name one account.What changed
@objectstack/plugin-auth,auth-manager.ts, at the/sign-in/emaillockout seam:findSignInAccount(engine, email, fields), is the one answer to "which account does this password sign-in name". It askssys_userexactly what better-auth 1.7.3's/sign-in/emailcredential check asks:internalAdapter.findUserByEmail(email.toLowerCase()), an equality match on the lower-cased address, which is the spellinginternalAdapter.createUserstores.assertAccountNotLocked), the counter (recordSignInOutcome) and the account events (resolveSignInAccountId) all read the account through it. Each keeps its own failure posture: the gate fails open, and the counter and the ledger report a lost write aterror, as before.sys_userrow (failed_login_count,locked_until), and every write to it is by the row's id, so one account holds one counter. There is nothing to migrate.as anycasts, soscripts/query-options-erasure-baseline.jsonrecordsauth-manager.tsat 12, down from 14 (pnpm check:query-options-erasure --update).Docs.
content/docs/permissions/authentication.mdxstates that the password-stage counter and lock belong to the account the sign-in's credential check resolves the address to.Changeset.
.changeset/auth-lockout-resolved-account.md, a@objectstack/plugin-authpatch.Tests
packages/plugins/plugin-auth/src/account-lockout-resolved-account.test.ts(new) drives a realAuthManagerand better-auth pipeline throughhandleRequest, over the in-memory engine this package's better-auth suites share (its equality is exact, as a SQL column's is by default). The threshold is 3, and the lock is read back from thesys_userrow. It pins:login_failedand then oneaccount_locked, all naming the account, and the event'slocked_untilequals the stored lock;ACCOUNT_LOCKED; each refusal recordslogin_failedwith reasonACCOUNT_LOCKED, no session is minted, and the stored lock does not move;sys_userrow and writes no account event, and the identifier never reaches the audit sink.The existing lockout unit cases in
auth-manager.test.ts, the account-event cases inauth-account-event-audit.test.tsand the durability cases indurability-swallow-repair.test.tspass unchanged.Ablation, run once on the committed fix with
scripts/ablation-replace.mjsin wrap mode. One mutation offindSignInAccount's query turned 5 cases red, across the new file andauth-account-event-audit.test.ts. The leg was restored, with the blob equal to HEAD and an emptygit diff HEAD. The mutation's detail is withheld under the disclosure ruling.Gates (local, at
94ea60bd5, after mergingorigin/mainatc3a8e07b6)pnpm --filter '@objectstack/plugin-auth...' build, which exited 0.pnpm --filter @objectstack/spec check:generatedreported all 14 generated artifacts up to date.pnpm --filter @objectstack/plugin-auth typecheckexited 0. It includestsconfig.examples.jsonandcheck:test-typecheck, whose test layer compiles the new file with no new debt.pnpm --filter @objectstack/plugin-auth testexited 0: 169 files, 3240 passed and 10 skipped.node scripts/pm/dispatch-gates.mjs --commandsderived 109 families for this change set, and all were run at the final head. The--ranreconciliation, with every exit code recorded, reads: 109 derived, 108 run with exit 0, 1 NOT MEASURED, 0 unrun.node scripts/check-plugin-teardown-shape.mjs --self-testexited 3. Its positive control is pinned to a commit that this shallow checkout cannot reach. CI runs it.test:repotask (the vitestrepoproject) was not run locally: the diff touches no test of that project, and CI'sTest Coreruns it.Generated by Claude Code