Repository navigation
Commit 72c0152
fix(objectql): a secret field's sys_secret row is written only with its record, and released when the column lets go (#27572)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuVpEmthvkRbuHGjC6oSQZvd)_
Fixes #27495
Clause-②: no
Metadata-protocol: none
Skills-docs: none — nothing a metadata author writes changes; the fix is
inside the engine's credential write path, and the documented `os secret
orphans` report is unchanged.
## 维护者速读
- 改了什么:`secret` 字段的 `sys_secret`
凭据行改为只随记录本身的写入一起落库;写入没完成时收回它已写的凭据行;更新把字段清为 `null` 或换成新值后,释放旧值的凭据行。
- 为什么改:凭据行原先在校验之前就写入,写入被拒也留下无人引用的密文;清空字段也不释放旧行。用户以为没存或已删的密文其实还留在库里。
- 风险与回滚:释放前先核实三件事:同一列没有任何行还引用这个句柄,句柄是这一列自己的,删除跟随记录所在的事务。核实不了就保留该行并记一条
error 日志,这是可恢复的方向。读出的打码、`resolveSecretField`、调用方收到的拒绝都不变;历史遗留的孤儿行不清理,仍由
`os secret orphans` 报告。回滚只需 revert 本 PR。
- 先看:`packages/objectql/src/engine-secret-internal-fields.ts`(风险在
`releaseSecretHandles`
的核实与事务判断),`packages/objectql/src/engine.ts`(insert / update
的写入点与两处释放点),`packages/objectql/src/secret-field-write-commit.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run
src/secret-field-write-commit.test.ts`,预期 21/21 通过。
- 需要你做的:无,席位落地。
## What changes
The credential pass for a `secret` field is split into three steps on
the existing secret-fields path (`engine-secret-internal-fields.ts`,
whose bodies `engine.ts` calls).
- **Seal, before validation, as before.** `encryptSecretFields` still
encrypts the value and puts the `secret:` ref on the row, so validation
and the post-image permission check judge the stored reference exactly
as they did. It no longer writes the `sys_secret` row; it hands the
sealed row back. The fail-closed refusals (no crypto provider, no
`sys_secret` store, the empty string) are unchanged and still fire at
this point.
- **Persist, with the record.** `persistSecretRows` writes the sealed
rows immediately before the record's own driver statement, after every
pass that can refuse the write: on insert, only for the rows
`insertMany` keeps; on update, on both the by-id and the predicate
branch.
- **Release, verified.** `releaseSecretHandles` runs on two occasions.
- `refused`: a write that did not complete (validation, an option
outside the list, a required field, a hook, the permission check, the
driver refusing the statement) takes back the rows it wrote.
- `retired`: a committed update that clears a `secret` field with
`null`, or replaces its value, releases the handle the field named
before. The candidates come from the by-id pre-image, or from the
predicate path's prior rows (the D7 memo when it was read, else one
narrow `id` + secret-columns read). An echoed ref or an echoed mask
releases nothing. It runs after the `afterUpdate` chain, so a refusal
that rolls the record back never reaches it.
- Each candidate is checked against storage before anything is deleted.
No row of that column may still name the handle (a predicate update
shares one handle across every row it matched). The `sys_secret` row's
`namespace` / `key` must be this object and field. The delete rides the
record's transaction: the explicit, still-open handle, else the ambient
unit. If the record write rode a unit that does not cover `sys_secret`,
the row is kept, because the record could still roll back and name it.
- Any failure keeps the row and logs one `error` line that names the
column, never the handle id, with the driver fault redacted, and points
at `os secret orphans`. The caller's answer never changes.
- **Not in this change.** Legacy orphans are not swept; they stay
report-only, per #8259. Reads, `resolveSecret` / `resolveSecretField`,
the mask round trip and every refusal envelope are unchanged. Deleting a
record is not touched.
## Tests
- `packages/objectql/src/secret-field-write-commit.test.ts` (new, 21
cases): a real engine over a recording driver with snapshot
transactions.
- A refused write leaves the `sys_secret` count unchanged: insert
refused by a required field, an option outside the list, a
`beforeInsert` hook, the post-image permission check, the driver
refusing the statement, a batch statement, and a partial `insertMany`;
update refused by an option outside the list, the permission check, the
driver; a predicate update refused by an option outside the list and by
the driver; an `afterInsert` abort.
- A committed write still stores, masks and dereferences; an echoed mask
releases nothing.
- A `null` clear removes the old row; a replacement removes the old row
and keeps the new one; a predicate clear removes every matched row's
handle; a handle a predicate write shared outlives every row but the
last; a handle minted for another column is never released through this
one; a release that cannot confirm keeps the row and logs one line
naming the column and not the handle.
- `packages/qa/dogfood/test/secret-field-orphan-rows.dogfood.test.ts`
(new, 5 cases): the same halves at the REST data door of a booted stack
with a provider wired, counting `sys_secret` rows at the driver.
## Acceptance notes
- The card asks for every refusal class. Three of them (a `before*`
hook, the insert-side permission check, an `after*` abort) already wrote
no row before this change, because they refuse before the credential
pass or roll back with the record. Their cases were green on the unfixed
tree and hold that order; the other classes were red there (15 of 21).
- Replacing a value is the same mechanism as clearing it (the column
lets go of the old handle), so both are released.
- The claim's file surface names `secret-fields.ts` and the engine's
write and clear steps. Since the engine split, those steps' bodies live
in `engine-secret-internal-fields.ts`; `secret-fields.ts` itself needed
no change.
- This branch is the card thread's harness-pinned
`claude/project-thread-r4cp59`; the claim on #27495 names it.
## Verification
All readings below were taken at head `c0b6bfc7`, after `origin/main`
(`d4ff1a02`) was merged in and the dogfood dependency closure was
rebuilt, unless a line says otherwise.
<details><summary>Unit, typecheck, lint and the REST door</summary>
- Red first, on the unfixed tree: `secret-field-write-commit.test.ts`
showed 15 failed / 6 passed (21), each failure on a `sys_secret` count.
- `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=3`:
Test Files 487 passed (487), Tests 9143 passed (9143).
- `pnpm --filter @objectstack/objectql typecheck`: exit 0;
`check:test-typecheck: OK`, 39 files / 233 errors / 64 pinned signatures
held, unchanged.
- `eslint` on the three changed `packages/objectql` files: exit 0.
- `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2
test/secret-field-orphan-rows.dogfood.test.ts`: Tests 5 passed (5).
- `pnpm --filter @objectstack/dogfood typecheck`: exit 0. `eslint` on
the dogfood file: exit 0.
- The full dogfood battery is not derived here: the title has no `!` and
`Clause-②` has no `(narrowing)` arm.
</details>
<details><summary>Ablation (each through scripts/ablation-replace.mjs,
restore proven)</summary>
Source-resolved, against `secret-field-write-commit.test.ts`; every
mutation landed (anchor 1 → 0) and every restore came back to the HEAD
blob `163a38d28da2` with `git diff HEAD` empty.
- The seal also writes the row (the old timing): 10 failed / 11 passed.
Every refused-write case the fix moved went red.
- The release's delete made a no-op: 8 failed / 13 passed (the clear,
replace, predicate-clear and shared-handle cases, and the four
driver-refusal take-backs).
- The shared-handle check removed: 1 failed (the shared-handle case).
- The column-ownership check removed: 1 failed (the foreign-column
case).
Dist-resolved, which the dogfood file consumes (taken at `33868e7d`; the
one later commit touches only the unit test's double): each mutation
carried a marker, `@objectstack/objectql` was rebuilt and
`ablation-dist-preflight` found the marker in `dist/`.
- The old write timing: 2 failed / 3 passed (both refused-write cases).
- The release's delete made a no-op: 2 failed / 3 passed (the clear and
replace cases).
- Restore: rebuilt, `--absent` clean for both markers, the file 5 passed
(5), and the tree clean against HEAD.
</details>
<details><summary>Local gate subset</summary>
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 67 commands at `c0b6bfc7`. All 67 were run, each
exit code captured before any pipe: 67 × exit 0. `--ran` reconciliation:
67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.
- On the first pass, at `33868e7d`, two gates did not pass.
`check:objectql-double-limit` flagged that the new test's recording
double ignored the caller's `limit`; it now applies the bound after its
filter (`c0b6bfc7`). `check:dual-build-cjs-loads` answered exit 3 (seven
packages had no `dist/`); those were built, and both are exit 0 above.
</details>
<details><summary>Overlap with open PRs</summary>
`git merge-tree` of this head against every open PR whose diff touches
these files: #27477, #27541 and #27543 (all on `engine.ts`) merge clean.
No other open PR touches them. This head also merges clean with the
current `origin/main`.
</details>
The required CI families run on this PR's check-runs and are not re-run
here.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes on the changed files.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
dispatch's required Chinese summary).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f557d90 commit 72c0152
5 files changed
Lines changed: 834 additions & 33 deletions
File tree
- .changeset
- packages
- objectql/src
- qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
18 | 25 | | |
19 | 26 | | |
20 | 27 | | |
21 | 28 | | |
22 | | - | |
| 29 | + | |
23 | 30 | | |
24 | 31 | | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
31 | 38 | | |
32 | 39 | | |
33 | 40 | | |
| |||
40 | 47 | | |
41 | 48 | | |
42 | 49 | | |
| 50 | + | |
43 | 51 | | |
44 | | - | |
| 52 | + | |
45 | 53 | | |
46 | 54 | | |
47 | 55 | | |
| |||
68 | 76 | | |
69 | 77 | | |
70 | 78 | | |
| 79 | + | |
71 | 80 | | |
72 | 81 | | |
| 82 | + | |
73 | 83 | | |
74 | 84 | | |
75 | 85 | | |
76 | 86 | | |
77 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
78 | 108 | | |
79 | 109 | | |
80 | 110 | | |
81 | 111 | | |
82 | 112 | | |
83 | 113 | | |
84 | 114 | | |
85 | | - | |
86 | | - | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
87 | 118 | | |
88 | 119 | | |
89 | 120 | | |
| |||
97 | 128 | | |
98 | 129 | | |
99 | 130 | | |
100 | | - | |
| 131 | + | |
101 | 132 | | |
102 | 133 | | |
103 | 134 | | |
| |||
129 | 160 | | |
130 | 161 | | |
131 | 162 | | |
132 | | - | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
133 | 166 | | |
134 | | - | |
| 167 | + | |
135 | 168 | | |
136 | 169 | | |
137 | 170 | | |
138 | 171 | | |
139 | 172 | | |
140 | 173 | | |
141 | 174 | | |
142 | | - | |
143 | | - | |
144 | | - | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
145 | 182 | | |
146 | 183 | | |
147 | 184 | | |
| |||
151 | 188 | | |
152 | 189 | | |
153 | 190 | | |
154 | | - | |
155 | | - | |
| 191 | + | |
| 192 | + | |
156 | 193 | | |
157 | 194 | | |
158 | 195 | | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
159 | 309 | | |
160 | 310 | | |
161 | 311 | | |
| |||
0 commit comments