Skip to content

test(auth): cover the better-auth 2FA lockout path; regenerate object translation bundles (#3624 follow-up) - #3659

Merged
os-zhuang merged 2 commits into
mainfrom
claude/org-creation-http-500-teams-gocypm
Jul 27, 2026
Merged

test(auth): cover the better-auth 2FA lockout path; regenerate object translation bundles (#3624 follow-up)#3659
os-zhuang merged 2 commits into
mainfrom
claude/org-creation-http-500-teams-gocypm

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#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:

table sys_two_factor has no column named failedVerificationCount

也就是说 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 漂移(移除 agent visibilitycapabilities.trash/mru,新增 summaryOperations.*)—— 那是对别人的 spec 改动所产生的、删除已有翻译的变更,应该由那个改动的作者来处理,不该混进这里。

测试

套件 结果
@objectstack/dogfood 358 passed, 3 skipped (63 files)
@objectstack/plugin-auth 522 passed (23 files)
@objectstack/platform-objects 215 passed (5 files)

配套的 cloud 侧多组织验证在 objectstack-ai/cloud 另开 PR。

Refs #3624, #3647


Generated by Claude Code

… 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
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 1:17pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, packages/qa.

4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authorization.mdx (via packages/qa)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 27, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 13:30
@os-zhuang
os-zhuang merged commit 9aa5510 into main Jul 27, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/org-creation-http-500-teams-gocypm branch 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants