|
84 | 84 | * - **by-id** (`delete(obj, { where: { id } })` — what better-auth's adapter |
85 | 85 | * emits, and what every cascade recursion re-enters with): `input.id` |
86 | 86 | * carries the scalar id. |
87 | | - * - **predicate / `multi`**: `input.id` is unbound and the row-scoping |
88 | | - * predicate rides on `input.options.where` — the same shape #5273 pinned |
89 | | - * for update. |
| 87 | + * - **predicate / `multi`**: `input.id` is present-but-undefined, and the |
| 88 | + * CALLER's own options bag is still on `input.options`, predicate included |
| 89 | + * — `delete()` only rebuilds that slot into `DriverOptions` *after* the |
| 90 | + * `before*` hooks return. That is the same slot the ban half reads, and it |
| 91 | + * does not contradict the `HookContextSchema.input` contract table |
| 92 | + * (#5273 / #5899): what is unreachable from `input` is the composed |
| 93 | + * `ast` — the *effective* predicate, onto which the filters middleware may |
| 94 | + * add RLS / sharing scoping. Middleware can only NARROW it, so treating the |
| 95 | + * caller's predicate as the doomed set over-approximates it, which is the |
| 96 | + * fail-closed direction: this guard may refuse a delete that would have |
| 97 | + * removed fewer rows, and can never miss one that removes more. |
90 | 98 | * - `ctx.previous` (the engine's #5272 pre-image, and objectql's |
91 | 99 | * `sys_fetch_previous_delete` builtin — `object: '*'`, priority 5) is bound |
92 | 100 | * for the by-id shape ONLY; a batch dispatch names no single row, so it |
@@ -341,8 +349,10 @@ export function registerLastAdminGuard( |
341 | 349 | * Which `sys_user` rows this one write addresses — the same answer for both |
342 | 350 | * halves. A scalar id when the engine dispatched by id (an update payload |
343 | 351 | * also carries it in `data.id`; a delete's `input` has no `data` at all), |
344 | | - * and otherwise the row-scoping predicate on `input.options.where`: the |
345 | | - * shape #5273 pinned for update and the one measured on `beforeDelete`. |
| 352 | + * and otherwise the caller's predicate, still on `input.options.where` while |
| 353 | + * `before*` runs (see the header: the composed `ast` is the part hooks |
| 354 | + * cannot read, and middleware may only narrow it — so this set is an |
| 355 | + * over-approximation, the safe direction). |
346 | 356 | */ |
347 | 357 | const resolveTargetIds = async ( |
348 | 358 | op: GuardedOp, |
|
0 commit comments