test(auth): cover the better-auth 2FA lockout path; regenerate object translation bundles (#3624 follow-up) - #3659
Merged
Conversation
… translation bundles (#3624 follow-up) Follow-up to #3647, which provisioned four better-auth 1.7 columns that no platform object had. Two loose ends from that PR. ## The 2FA lockout path had no test at all `sys_two_factor.failed_verification_count` / `locked_until` were added blind alongside the team fix — found by the new parity gate, not by anything exercising the behavior. That is why they went missing in the first place: the whole path was untested, so a column better-auth writes on every wrong code could vanish without a single failure. The new dogfood test pins the behavior, not just the schema: a wrong code is COUNTED (the counter advances in the database), and a correct code RESETS the count — which is what makes the budget track *consecutive* failures. It has to go through the mid-sign-in two-factor cookie, not a bearer token: better-auth only runs `assertTwoFactorNotLocked` / `recordTwoFactorFailure` when `isSignIn` is true, and deliberately skips both when the same endpoint is called with a live session. A bearer-token test would have passed while the lockout path stayed entirely unexercised — the exact blind spot that let the columns through. Reverting the `AUTH_TWO_FACTOR_SCHEMA` mapping makes it fail, and shows the breakage was wider than the wrong-code path: `two-factor/enable` itself 500s with `table sys_two_factor has no column named failedVerificationCount`, so enrollment was broken too. TOTP is generated in-test from the otpauth:// secret via RFC 6238 over node:crypto rather than importing `@better-auth/utils/otp` — that is a transitive dependency, and taking a direct one on it to produce six digits would tie the test to an internal package's resolution. ## Translation bundles `os i18n extract` regenerated for the four new fields, which were absent from every locale bundle. Also picks up `sys_organization.parent_organization_id` / `sort_order` from the earlier ADR-0105 D6 work, which had the same gap. The `*.metadata-forms.generated.ts` bundles are deliberately NOT included: the same run rewrites them with unrelated spec drift (agent `visibility` and `capabilities.trash`/`mru` removed, `summaryOperations.*` added). Those are DELETIONS of curated translations for a spec change this branch has nothing to do with, so they are left for whoever owns that change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYLC8TfjzHGwatNxZKdX7H
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…follow-up) The regenerated bundles ship inside @objectstack/platform-objects, so the change is consumer-visible and takes a real patch changeset rather than an empty one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYLC8TfjzHGwatNxZKdX7H
os-zhuang
marked this pull request as ready for review
July 27, 2026 13:30
os-zhuang
added a commit
that referenced
this pull request
Jul 27, 2026
#3624 follow-up) (#3667) #3659 pinned the two ends of the 2FA failure budget — a wrong code is counted, a correct one resets it. It stopped short of the transitions that make it a LOCKOUT rather than a counter, which is the part that actually protects the account. Three more assertions: 1. **The lock engages.** Spending the budget stamps `locked_until` in the future. Reaching it means respecting two stacked budgets: better-auth allows 5 verifications per two-factor cookie (`beginAttempt(5)`) and 10 consecutive failures per ACCOUNT (`accountLockout.maxFailedAttempts`). Only the second touches the columns under test, so the loop takes a fresh challenge every 5 tries. The counter is asserted after every single failure, not just at the end. 2. **The lock bites before the code is checked.** A locked account refuses even a VALID TOTP with 429. Without this the "lockout" would just be a slower guess loop. 3. **An expired lock clears itself.** better-auth clears it through a GUARDED `incrementOne` — `where locked_until <= now`, `set { failedVerificationCount: 0, lockedUntil: null }`. That is the only place the ObjectQL adapter has to handle `lte` against a datetime, so this covers the operator as much as the columns: a driver mishandling it would leave the user locked out forever with no error anywhere. The test ages the lock through the data engine because no endpoint can — the default duration is 900s. Test-only, and `@objectstack/dogfood` is private, hence the empty changeset. Claude-Session: https://claude.ai/code/session_01UYLC8TfjzHGwatNxZKdX7H Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang
added a commit
that referenced
this pull request
Jul 27, 2026
…undles (#3670) Finishes what #3659 deliberately left alone. That PR regenerated only the `*.objects.generated.ts` bundles and explicitly EXCLUDED the metadata-forms ones, because the same run deleted keys and I had not yet checked whether it was discarding curated translations. It was not — I checked: capabilities.trash / capabilities.mru enable.trash/mru retired in 16.x (#2377); the schema now REJECTS them, with tombstone guidance in UNKNOWN_KEY_GUIDANCE. agent `visibility` removed 2026-07 (#1901). Those are the only deletions in the whole diff, and all three are translations for keys no author can write any more. Nothing live is lost. The same sweep also picks up what the bundles never learned: summaryOperations.{object,function,field,relationshipField,filter} sys_invitation.{business_unit_id,positions} (ADR-0105 D8) and fixes a third, separate problem: `sys_migration`'s labels and help text sat committed as EMPTY STRINGS in the ja-JP and es-ES bundles. An empty string is not an untranslated key — it renders blank, so that object had no readable labels at all in those locales. They now carry the schema text like every other untranslated key. Run in merge mode, so every existing translation is preserved; the only removals are the three dead keys above. Claude-Session: https://claude.ai/code/session_01UYLC8TfjzHGwatNxZKdX7H Co-authored-by: Claude <noreply@anthropic.com>
21 tasks
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.
#3647 的两条收尾。那个 PR provision 了四个 better-auth 1.7 的列,但留下两个口子。
1. 2FA 锁定路径此前零测试覆盖
sys_two_factor.failed_verification_count/locked_until是被新 parity gate 静态发现的,不是被任何跑这条路径的测试发现的 —— 这正是它们当初能漏掉的原因:整条路径没有测试,所以一个 better-auth 每次输错验证码都要写的列可以凭空消失而没有任何东西变红。列是跟着 team 那半边一起盲加的。新增的 dogfood 测试钉的是行为,不只是 schema:
为什么必须走登录 cookie 而不是 bearer token
better-auth 只在
isSignIn为真时跑assertTwoFactorNotLocked/recordTwoFactorFailure,也就是请求经由登录中途的 two-factor cookie、没有活动 session 时。同一个端点带着活动 session 调用(注册确认路径)会故意跳过这两个函数。所以拿 bearer token 写的测试会通过,而锁定路径完全没被碰到 —— 那正是当初让这两个列漏过去的盲区。文件里那段 cookie 处理不是仪式,是这个测试能测到东西的前提。
反向验证
把
AUTH_TWO_FACTOR_SCHEMA的映射撤掉,测试会红 —— 并且暴露出破损范围比预想的更大:two-factor/enable本身就 500:也就是说 2FA 注册也是坏的,不只是输错验证码那条路径。
关于 TOTP
六位码在测试内用 node:crypto 按 RFC 6238 现算,没有引入
@better-auth/utils/otp—— 那是个间接依赖,为了生成六位数字而对它建立直接依赖,会把测试绑在一个内部包的解析方式上。参数(SHA-1 / 6 位 / 30 秒)由enable自己返回的 otpauth:// URI 佐证。2. 翻译 bundle
os i18n extract重新生成:四个新字段此前在所有 locale bundle 里都不存在。顺带补上了sys_organization.parent_organization_id/sort_order—— 那是更早的 ADR-0105 D6 留下的同类缺口。*.metadata-forms.generated.ts刻意没有包含进来。 同一次 extract 会把它们一起改写,但改的是与本分支无关的 spec 漂移(移除 agentvisibility、capabilities.trash/mru,新增summaryOperations.*)—— 那是对别人的 spec 改动所产生的、删除已有翻译的变更,应该由那个改动的作者来处理,不该混进这里。测试
@objectstack/dogfood@objectstack/plugin-auth@objectstack/platform-objects配套的 cloud 侧多组织验证在 objectstack-ai/cloud 另开 PR。
Refs #3624, #3647。
Generated by Claude Code