Skip to content

HookContext 契约表把 before*input.options 记成 DriverOptions —— 实测那里仍是调用方的 engine options(含 where),两个 break-glass 守卫正读它 #5997

Description

@baozhoutao

观察类发现(finding),来自 #5941(break-glass delete 守卫)的实测。今天没有用户会撞上:代码行为正确且被两个守卫依赖,问题在于同一段散文对它的描述不精确,而这段散文就是下一个作者能读到的唯一规格。

两条陈述

packages/spec/src/data/hook.zod.tsHookContextSchema.input 契约表(#5273 / PR #5668 落地,#5964 刚把枚举注释对齐到它):

   * - delete (bulk, multi:true) — before: { id: undefined, options: DriverOptions }
   * - update (bulk, multi:true) — before: { id: undefined, data: Record, options: DriverOptions }
   ...
   * The row-scoping predicate is NOT reachable from `input` at all. …
   * scope the batch through `options.where` at the CALLER, or work per row on
   * the `after*` events below.

实测(对 origin/main = ffd51fd7a)

packages/objectql/src/engine.ts:

事实
5516 → 5517 await this.triggerHooks('beforeUpdate', …) 之后hookContext.input.options = this.buildDriverOptions(…)
6137 → 6152 await this.triggerHooks('beforeDelete', …) 之后才做同一件事

也就是说 before* 期间 input.options 仍是调用方那只 engine options 包(EngineUpdateOptions / EngineDeleteOptions),wheremulti 都在;它变成 DriverOptions 是在钩子返回之后、驱动调用之前。真engine + better-sqlite3 上探针实测到的 beforeDelete 载荷:

by-id      : { inputId: 'u1', inputOptionsWhere: { id: 'u1' }, previous: [...] }
multi      : { inputId: undefined, inputOptionsWhere: { id: { $in: ['u2','u3'] } }, inputOptionsMulti: true }

所以契约表里两处 before 行的 options: DriverOptions 与实测不符,而「NOT reachable from input at all」+「scope … at the CALLER」这组措辞,读起来像是「钩子看不到谓词」——钉子测试并没有钉这一句:hook-input-shape-contract.test.ts 断言的是 'ast' in input === false(以及读路径的阳性对照),没有任何一条断言 input.options 上没有 where

为什么值得记

这不是措辞洁癖:packages/plugins/plugin-auth 的两个 break-glass 守卫(#5892 的 ban 半边、#5941 的 delete 半边)正是before* 期间的 input.options.where 解析谓词/multi 写的目标行集 —— 没有它,批量写这条能一次扫掉全部管理员的路径就是盲区。按现在的散文,下一个安全钩子作者会得到「谓词拿不到,放弃」的结论,或者反过来读到守卫的代码后认为它违反契约。

两句都可以同时为真,只是要把区别写明:钩子拿不到的是 composed ast(生效谓词,filters 中间件可能往上叠 RLS / sharing 的收窄);拿得到的是调用方原始 options.where。因为中间件只会收窄不会放宽,把调用方谓词当作行集是上界近似 —— 对 fail-closed 的守卫恰好是安全方向。

建议

契约表两处 before 行改成实测形状(调用方 engine options,DriverOptionsafter* / 驱动调用起才成立),并在那段说明里补一句区分 composed ast 与调用方 options.where;顺手给 hook-input-shape-contract.test.ts 加一条正向断言(before*input.options.where 就是调用方传入的谓词),这样这条被两个安全守卫依赖的性质从散文变成钉子。

参考

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions