|
| 1 | +--- |
| 2 | +"@objectstack/plugin-audit": patch |
| 3 | +--- |
| 4 | + |
| 5 | +fix(plugin-audit): consume the engine's bound `ctx.previous` and record one normalised view on both sides of the diff (#6656) |
| 6 | + |
| 7 | +`plugin-audit` used to fetch its own pre-image. `captureBefore`, registered on |
| 8 | +`beforeUpdate` / `beforeDelete`, issued a `ql.findOne` for the target row and |
| 9 | +stashed it on `ctx.__previous`, because `HookContext.previous` was "officially |
| 10 | +typed but not always populated by the engine itself". That is no longer true on |
| 11 | +any path this plugin registers for, so the read is retired and the writer reads |
| 12 | +the contract value. |
| 13 | + |
| 14 | +**The read that goes away** (measured with a counting driver on the audited |
| 15 | +object, `driver.findOne` per write): |
| 16 | + |
| 17 | +| write | before | after | |
| 18 | +|:--|--:|--:| |
| 19 | +| single-id `update()` | 2 | 1 | |
| 20 | +| single-id `delete()` | 2 | 1 | |
| 21 | +| predicate `update()`, 3 matched rows | 3 | 0 | |
| 22 | +| predicate `delete()`, 3 matched rows | 3 | 0 | |
| 23 | + |
| 24 | +The predicate column is the larger half and was pure waste. #5574 binds |
| 25 | +`input.id` on every per-row *before* context, which defeated the handler's own |
| 26 | +`if (!id) return` bulk guard — so it read every matched row, and every result |
| 27 | +was discarded, because `__previous` landed on the per-row *before* context while |
| 28 | +the per-row *after* contexts (the ones the writer actually runs on) never saw |
| 29 | +it. The engine's own matched-row read is untouched and still serves both phases, |
| 30 | +so the ledger is unchanged. |
| 31 | + |
| 32 | +**What the ledger records changes, and deliberately.** The two sides of an audit |
| 33 | +diff came from two different pipelines: `before` through the engine's read path |
| 34 | +(credentials masked, formulas hydrated, file references resolved) and `after` |
| 35 | +from the raw write result. That asymmetry — not the redundant read — is why a |
| 36 | +write that touched one field recorded phantom "changes" for every secret, file |
| 37 | +and formula field on the record. Retiring the read makes both sides |
| 38 | +same-source; the writer now also gives them one view, so the surface levels |
| 39 | +upward rather than down to raw store contents: |
| 40 | + |
| 41 | +- **Credential fields are masked on both sides.** Single-id delete `old_value` |
| 42 | + still reads `••••••••` for a `secret` field — that face is byte-identical. |
| 43 | + Change detection still runs on the raw values, so rotating a secret is still |
| 44 | + recorded as a change; only the recorded values are masked. |
| 45 | +- **A pre-existing leak is closed.** The stored `secret:` ref was already |
| 46 | + reaching `sys_audit_log.new_value` on every create and update, and a |
| 47 | + `password` field — which ADR-0100 stores in cleartext at rest — was landing |
| 48 | + there **in plaintext**, in the audit ledger and in the `sys_activity` summary |
| 49 | + rendered in the record feed. Both now record the mask. |
| 50 | +- **Virtual (`formula`) fields leave the full snapshots.** `ctx.result` carries |
| 51 | + hydrated formulas (#5504) and the raw pre-image structurally cannot, so |
| 52 | + create `new_value` would have described a field delete `old_value` could |
| 53 | + never carry. Only genuinely virtual fields are dropped: `autonumber` and |
| 54 | + `summary` are stored columns present and equal on both sides, and they stay |
| 55 | + in the snapshot. |
| 56 | + |
| 57 | +Two consequences worth naming, both narrowing single-id delete to what bulk |
| 58 | +delete already did: its `old_value` now records a file field's stored id rather |
| 59 | +than the resolved `{id, name, size, url}` object, and drops formula values. An |
| 60 | +object whose label field is a formula falls back to the record id in the |
| 61 | +`sys_activity` label on delete for the same reason. |
| 62 | + |
| 63 | +No audit coverage is removed: the plugin keeps its `afterInsert` / `afterUpdate` |
| 64 | +/ `afterDelete` registrations, which is what holds the engine's pre-image demand |
| 65 | +gates open, and every one of them keeps the `excludeObjects` face from #5860. |
0 commit comments