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.ts 的 HookContextSchema.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),where 与 multi 都在;它变成 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,DriverOptions 是 after* / 驱动调用起才成立),并在那段说明里补一句区分 composed ast 与调用方 options.where;顺手给 hook-input-shape-contract.test.ts 加一条正向断言(before* 的 input.options.where 就是调用方传入的谓词),这样这条被两个安全守卫依赖的性质从散文变成钉子。

参考

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions