fix(plugin-auth): better-auth 的 contains 下译为 $contains,比较值不再当正则求值 (#5710) - #5812
Merged
Conversation
…t a bare `$regex` (#5710) `convertWhere()` emitted `{ field: { $regex: value } }` for better-auth's `contains`, putting an unescaped caller-supplied comparand into a PATTERN position on the authentication path. driver-memory evaluated it as a real RegExp (`a.b` matched `axb`; an unbalanced `(` threw on the mingo path and silently matched nothing on the reference matcher), while driver-sql / -sqlite-wasm / -turso compiled it to an escaped substring LIKE — one auth query, two answers. `$contains` is in the spec's FILTER_OPERATORS and means a literal substring on every backend, which is what better-auth's `contains` means (`Where.mode` defaults to `"sensitive"`, matching the #5701 Q2=A ruling). This also retires the last live producer of `$regex`, which is the ordering constraint #5702's loud refusal waits on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…of erasing it to `any` (#5710) The new pin's read-only engine facade forwarded its query with `as any`, which the query-options-erasure ratchet counts on the test surface too (267 -> 270). Declare the driver-side `QueryAST` and forward it unchanged — nothing here is deliberately off-contract, so no erasure is warranted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
baozhoutao
marked this pull request as ready for review
August 6, 2026 06:33
This was referenced Aug 6, 2026
Closed
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.
Fixes #5710
packages/plugins/plugin-auth/src/objectql-adapter.ts的convertWhere()把 better-auth 的contains译成{ field: { $regex: value } },一个未转义、来自调用方的比较值坐进了正则的模式位,而且发生在认证路径上。本 PR 把这一支翻成$contains。前提复核(origin/main@
1624f4a)objectql-adapter.ts:137是filter[fieldName] = { $regex: condition.value }(翻转前)。driver-memory/src/filter-refusal.ts:145-158的SUPPORTED_FIELD_OPERATORS仍为new Set([...FILTER_OPERATORS, '$regex', '$options']),注释原话仍是「Refusing it here would break a live producer」。GET /api/v1/auth/admin/list-users(auth-route-ledger.ts:161)把searchOperator(默认contains)与filterOperator直接推进where(better-authdist/plugins/admin/routes.mjs:358-368)。目标算子:
$contains,不是$icontains(动手前验证的前提)派发令要求先验证「
$icontains仍是纯声明面」再动手 —— 已验证,前提成立:packages/spec/src/data/filter.zod.ts:1086-1096的FILTER_OPERATORS不含$icontains(只有$contains/$notContains/$startsWith/$endsWith);$icontains「joins this array in the PR that implements it (drivers:$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702), not before」;$icontains,driver-memory 的形状门会先拒收(fail-closed),而任何绕过拒收的面会走匹配器default: break—— 谓词被静默丢弃,过滤放大成全匹配,在认证读路径上是越权读。所以本单的目标算子是
$contains;$icontains的执行面归 #5702。大小写语义(派发令要求写进正文的判断)
上游期望:better-auth 的
Where类型自带mode?: "sensitive" | "insensitive",默认"sensitive"(@better-auth/core/src/db/adapter/index.ts:324-343)。也就是说 better-auth 的contains默认要的就是大小写敏感的字面子串,这与 #5701 Q2=A 裁定的$contains契约(大小写敏感)同向。spec 自己的退役处方也明写了这个二选一:RETIRED_FILTER_OPERATORS.$regex.why—— 「Write$icontainsfor the case-insensitive substring search …, or$containsfor a case-sensitive one」。一处要诚实纠正的说法:issue 正文与派发令都写了「裸
$regex在 memory 上本来也是大小写敏感的,所以翻转对该 driver 等价」。这句话只对 driver-memory 的参考匹配器(memory-matcher.ts的match(),用String.includes)成立,对查询路径不成立:normalizeFieldOperators(memory-driver.ts:939-941)把$contains降为new RegExp(escapeRegex(v), 'i')—— 转义 +i标志;$regex交给 mingo 时没有任何 flags,是大小写敏感的。所以在 driver-memory 的实际查询路径上,本次翻转把大小写行为从「敏感」改成了「不敏感(全 Unicode 折叠)」。这不是本 PR 引入的越契约行为,而是该 driver 今天既有的状态 ——
scripts/check-driver-conformance.mjs的 driver-memory DEBT 行已按测量写明这一点,并指向 #5702 去把它对齐回敏感。本 PR 不钉这条大小写行为(钉了会与 #5702 的对齐相冲突),只把它记录在此。反向验证:先红后绿(实测)
新 pin 先在未翻转的代码上跑,四条按预期红:
翻转后全绿(
Test Files 35 passed (35) / Tests 793 passed (793))。第四条要单独说明:issue 预测非法模式会「静默不匹配」,那是参考匹配器那一面的行为(
catch { return false });实测查询路径是直接抛SyntaxError(mingo 在编译Query时构造RegExp),连 ADR-0112 信封都没进。两种都是坏答案,只是坏法不同 —— 按实测记录,而不是按预测记录。测试落点
packages/plugins/plugin-auth/src/auth-contains-filter.test.ts,两面:{ name: { $contains: 'a.b' } },且整棵树里没有$regex;另加一条词表 pin(FILTER_OPERATORS含$contains、不含$regex)—— 这是「五后端都必须求值」这句话的凭据。InMemoryDriver,经createObjectQLAdapterFactory的 better-auth 适配器):a.b只命中a.b不命中axb;^a一行不中;(按字面命中x(y;无元字符的xb照旧命中axb。只有契约面看不出「拼对了但求值错了」,只有行为面说不清是哪个算子换来的结果,所以两面都要。测试没有落进 spec 的
FILTER_TEXT_CASES共享一致性表 —— 那是 #5701/#5702 的领地,这里钉的是 adapter 这一个实现面。为跑行为面,plugin-auth 增加了
@objectstack/driver-memory的 devDependency(不进发布产物)。测试里的引擎替身只声明三个读动词,写动词是刻意不声明的:种子数据直接经 driver 写入,避免手写一个本测试既不需要、也无法遵守的 delete/update dispatch 契约(check:engine-double-contract)。对 #5702 的影响(必答项)
变简单,且是「解锁」而非「减负」:#5702 的「校验期响亮拒收
$regex」原本被这一个活体生产者挡着(filter.zod.ts:1150-1155「Hard order: #5710 flips the producer, then #5702 turns these strings into refusals」、check-driver-conformance.mjs:279-282「#5710 flips that producer before any of these four cells may be cleared」)。本 PR 合并后:$regex生产者(全仓扫描:剩下的$regex出现点都是消费面、退役处方文本或拒收断言),四个 driver 的$regex求值臂可以按 drivers:$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702 计划直接删/改拒收,不再有「会打断登录」的顺序约束;driver-memory/src/filter-refusal.ts的SUPPORTED_FIELD_OPERATORS里$regex/$options两个额外成员,其唯一存在理由(注释里写死的那句)随之消失,可以整段删掉;$contains大小写对齐(drivers:$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702 预算内)的工作量不变,但现在多了一个受益者:认证路径的contains会随对齐一起从「memory 上不敏感」回到契约的敏感,与 better-auth 的mode: 'sensitive'默认对齐。一处给 #5702 的备注(不另立单,因为这几行正是 #5702 要改的):
packages/spec/src/data/filter-text-conformance.ts:272把{ $regex, $options }说成「The exact shape plugin-auth's adapter can emit」。实测该 adapter 从不发$options(它只写{ $regex: value }),所以那条用例钉的是一个本 adapter 产生不出来的形状 —— 用例本身仍有价值(拒收要同时点名两个拼写),只是这句归因不准。越界发现(已另立单,本 PR 不修)
同一个
convertWhere()上另有两个与本单不同类的缺陷,均按 Prime Directive #10 单独立单:convertWhere()对not_in/starts_with/ends_with没有分支,谓词被整条丢弃 —— 认证路径上的过滤放大 #5813:not_in/starts_with/ends_with三个 better-auth 算子在convertWhere里没有分支,谓词被整条丢弃 —— 过滤放大(admin list-users 的filterOperator/searchOperator是活体调用方)。convertWhere()整体忽略 better-auth 的Where.mode: 'insensitive'(SCIM 会发它) #5814:Where.mode: 'insensitive'被整体忽略(@better-auth/scim对caseExact: false的属性会发它;需要 drivers:$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702 的$icontains执行面才修得了)。验证命令