Repository navigation
fix(objectql): deleting a record releases the sys_secret row its secret field named - #27657
Conversation
Claude-Session: https://claude.ai/code/session_01L6bhgUFEohazYphk3zDAum Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgUFEohazYphk3zDAum Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cae6e057fd31542fbab006685407d65d8300cfee && git checkout cae6e057fd31542fbab006685407d65d8300cfee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b761968d9df2a2a72bbbdb96b4f67ca78a58739f b15352e9d73111c29ccd775cdaef2a50c3c5934b && git checkout -B drift-repro b761968d9df2a2a72bbbdb96b4f67ca78a58739f && git merge --no-ff b15352e9d73111c29ccd775cdaef2a50c3c5934b
node scripts/docs-audit/affected-docs.mjs --json b761968d9df2a2a72bbbdb96b4f67ca78a58739f |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。只把删除的候选交给现成的释放规则,没有第二条规则;释放点在 席位核对ACCEPT · head
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Requested by Sales · project thread
Fixes #27581
Clause-②: no
Metadata-protocol: none
Skills-docs: none — nothing a metadata author writes changes; the fix is inside the engine's delete path, and the documented
os secret orphansreport is unchanged.维护者速读
secret字段引用的sys_secret凭据行。覆盖按 id 删除、按条件批量删除、REST 的deleteMany,以及级联删除连带删掉的子记录。secret列,凭据密文留在库里,没有任何记录引用它,却仍可解密;凭据表随每次删除增长。packages/objectql/src/engine.ts(delete 流程里取句柄的三处,以及afterDelete之后的释放点),packages/objectql/src/engine-secret-internal-fields.ts(deletedSecretSlots),packages/objectql/src/secret-field-write-commit.test.ts(新增的 delete 一段)。pnpm --filter @objectstack/objectql exec vitest run src/secret-field-write-commit.test.ts,预期 30/30 通过,其中 9 条是本 PR 新增。What changes
A committed delete now releases every handle the deleted rows held in a
secretcolumn, through the release #27572 added for a committed update that clears the field. There is one release rule; this change only hands it the delete's candidates.deletedSecretSlots(inengine-secret-internal-fields.ts) lists each handle a deleted row held in asecretcolumn. It isretiredSecretSlotscalled with anullclear of every secret column, so a delete's candidates are exactly what clearing those columns would retire.id+ secret-columns read through the write's AST and driver options, before the statement. An object with nosecretfield reads nothing new.releaseSecretHandles(object, deletedSecrets, context, 'retired')runs after theafterDeletechain and its result-shape check, before the roll-up recompute. A hook can still dereference the value, and a delete that is refused or rolled back never reaches it. Each candidate goes only when no remaining row of its column names the handle, thesys_secretrow'snamespace/keyare that column's own, and the count and delete ride the record's unit. Otherwise the row is kept and oneerrorline names the column.os secret orphans, per [Decision] sys_secret orphan reaping: which vehicle carries a destructive sweep over stored credentials — migration, admin command, or opt-in script? #8259. What a delete answers and what reads return are unchanged.Tests
packages/objectql/src/secret-field-write-commit.test.ts, a new[#27581]block of 9 cases on the existing double.beforeDelete, and one rolled back by an abortingafterDelete; a release that cannot confirm, which logs one line naming the column and not the handle.deleteManyand an isolation mode where a rollback undoes only the writes made under that unit's handle, as a database does. In the default snapshot mode a rollback restores every store, so a release that escaped the unit could not be seen.packages/qa/dogfood/test/secret-field-delete-release.dogfood.test.ts(new, 4 cases), at the REST data door of a booted stack with a provider wired, countingsys_secretrows at the driver.DELETE /data/OBJECT/IDremoves that record's row and no other.POST /data/OBJECT/deleteManyremoves the row of every record it deletes.deleteManythat rolls back (its second id names no record) keeps the record and its row.Acceptance notes
multi: true) on three plans (no relation points at the object; a cascade inside one unit; a split cascade); cascade members, deleted by id. The RESTDELETE, the bulkdeleteManydoor (per-id engine deletes, atomic or not) and the cross-object/batchdelete all reach the engine's delete by id. There is no soft-delete or recycle-bin path:softDeleteandenable.trashwere retired from the spec in the 16.x line as never enforced, and no runtime keeps a deleted record, so there is no "kept record" case to protect.'retired'occasion, so a kept row logs the same line ("the sys_secret row that OBJECT.FIELD named before this write was not removed"). No second rule and no new log text.sqlite-wasmdriver, the release that does not ride the unit goes red by timeout (Test timed out in 5000ms), not by an assertion. The batch holds the pool's single connection, so a statement outside the unit waits for it. The assertion-level red for "the release rides the unit" is the engine case on the double's isolation mode (ablation A3 below).engine.ts. The delete flow, two names in the secret-fields import list, and one sentence inreleaseSecretHandles's docblock naming the delete occasion.engine-secret-internal-fields.tsgains the two small helpers and one header sentence.claude/project-thread-v9mp90; the claim on objectql: deleting a record does not release the sys_secret row its secret field names #27581 (6500734029) names it.Verification
Final head
b15352e9is54efb0b8withorigin/main(f7ff693c) merged in. The red-first and ablation legs ran at54efb0b8, with the fix committed. The final-head readings below were taken atb15352e9after rebuilding what the merge touched.Red first, unit, typecheck, lint and the REST door
engine.tsandengine-secret-internal-fields.tsat base9c513147, the new tests kept):secret-field-write-commit.test.tsshowed 6 failed / 24 passed (30). The 6 were the four released cases, the shared-handle case's last delete, and the cannot-confirm case. The 3 kept cases were green there, as a keep should be.pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=3atb15352e9: Test Files 488 passed (488), Tests 9163 passed (9163).pnpm --filter @objectstack/objectql typecheck: exit 0;check:test-typecheck: OK, 39 files / 233 errors / 64 pinned signatures held, unchanged.pnpm --filter @objectstack/dogfood typecheck: exit 0.pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/secret-field-delete-release.dogfood.test.ts test/secret-field-orphan-rows.dogfood.test.tsatb15352e9: Test Files 2 passed (2), Tests 9 passed (9).eslinton the four changed source and test files: exit 0.pnpm lint(the repo-wide run) atb15352e9: exit 0.!andClause-②has no(narrowing)arm.Ablation (each through scripts/ablation-replace.mjs, restore proven)
Source-resolved, against
secret-field-write-commit.test.ts. Every mutation landed (anchor 1 → 0), and every restore came back to the HEAD blob (engine.tsab2b884a7373,engine-secret-internal-fields.ts621c5ec3c95f) withgit diff HEADempty.Dist-resolved, which the dogfood file consumes. Each mutation carried a marker;
@objectstack/objectqlwas rebuilt andablation-dist-preflightfound the marker in 4 built files before the run.deleteManydoor, the shared handle's last delete).--absentexit 0 for all three markers, the file 4 passed (4), and the tree clean against HEAD.Local gate subset
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 67 commands atb15352e9. All 67 were run, each exit code captured before any pipe: 67 × exit 0.--ranreconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.check:dual-build-cjs-loadsanswered exit 3 on its first run (seven packages had nodist/); those were built, and it is exit 0 above.Overlap with open PRs
git merge-treeof this head against every open PR whose diff touches these files: #27477 (head5eb62f09, onengine.ts) merges clean. No other open PR touches them, measured over every open PR. The server seat 1 hot-file queue names #25522 and then #27165 forengine.ts; neither has an open PR. This head also merges clean with the currentorigin/main.The required CI families run on this PR's check-runs and are not re-run here.
Human Minimum
packages/specholds no runtime logic. (Not touched.)pnpm lintpasses.main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgUFEohazYphk3zDAum