Repository navigation
fix(service-settings): judge the whole fetched page when two data-key files differ, and report a mixed-key database - #30232
objectstack-fleet[bot] wants to merge 2 commits into
Conversation
… files differ, and report a mixed-key database The boot trial scored only the newest known-marker sys_secret row. It now scores both keys over the page the reader already fetched (up to ten rows), takes the key that opens more of them (a tie goes to the newest), reports a page split between the keys as a boot error naming both files, and lets an older row settle the choice when the newest row opens under neither key. Notices say what was measured. Review nits from the trial-open PR: the stale datasource-binder catch comment in serve.ts, the README file-tier line, and a JSDoc note that resolveExistingDataKey writes the channels on process.env. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CahkbD22YHPM8hMrA8
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 41 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 3a61034b5436427f3ada2bc18a07cfa112f14cba && git checkout 3a61034b5436427f3ada2bc18a07cfa112f14cba
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8f05e61bb33a17a196785d7df2109cdd81ee1e97 26eb6119c48ab35d966e4adfe199644779026e92 && git checkout -B drift-repro 8f05e61bb33a17a196785d7df2109cdd81ee1e97 && git merge --no-ff 26eb6119c48ab35d966e4adfe199644779026e92
node scripts/docs-audit/affected-docs.mjs --json 8f05e61bb33a17a196785d7df2109cdd81ee1e97
|
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6CahkbD22YHPM8hMrA8
Fixes #30091
Clause-②: no — an internal boot heuristic and its notice text. The shapes it changes (
StoredSecretReaderanswers the page,KeyFileChoicegains themixedreason and atally,DataKeyOpensNothingErrorgainsjudged) were added by #30042, whose changeset is still pending, so they first ship together in the same release; no released export changes, and no new error code is added.Metadata-protocol: none
维护者速读
sys_secret,而是对已取回的最新一页(最多 10 条)逐条用两把钥匙试开,用打开条数更多的那把(平局取能打开最新一条的那把);两把各开一部分时,按「混合密钥」打一条启动错误,点名两个文件、各开几条和修法;最新一条坏了但较早的行能开时,照常选出钥匙,不再在生产拒启。提示文案改为只说实测到的(「打开最新 3 条中的 2 条」),不再说「打开本库全部已存密钥」。另修三个评审小问题,并在运维文档加一段两文件规则。packages/services/service-settings/src/local-crypto-provider.ts的chooseKeyFileByTrialOpen(平局与mixed分支)与scoreKeyOverPage;同文件storedSecretReaderOverEngine(现返回整页);content/docs/deployment/backup-restore.mdx新增的那一段。pnpm --filter @objectstack/service-settings exec vitest run src/local-crypto-provider.test.ts,预期 74/74 通过,其中#30091 — the trial scores both keys over the whole page一组 8 条。What changes
StoredSecretReadernow answers the newest page, newest first (empty = no stored secret);storedSecretReaderOverEnginereturns every known-marker row of the page it already fetched (limit: 10, unchanged) instead of the first.chooseKeyFileByTrialOpenopens each row under both keys (scoreKeyOverPage, each scope as before) and counts.opens, that key's file. Both keys open rows: the newmixedreason, the file whose key opens more of them (a tie goes to the key that opens the newest of them, the key sealed with last), with anerrornotice naming both paths, both counts, the consequence and the fix, in every posture. No row opens under either key: unchanged (opens-nothing; production throwsDataKeyOpensNothingError, which now records and prints how many rows it judged). A row that opens under neither key no longer decides anything while another row opens: a corrupt newest row with an older row under one key settles on that key.KeyFileChoice.tally(judged,chosen,other) records what was measured whenever rows were judged.opensnotices said the chosen file "opens this database's stored secrets"; they now say what was measured ("opens the 3 newest stored secrets", "opens 2 of the 3 newest stored secrets", plus how many open under neither). The refusal reads "neither data key opens any of the N newest stored secrets".serve.ts: the datasource-binder comment and catch no longer claim a provider-construction error lands there (the deferred provider constructs nothing at that point; a key problem surfaces at the secret-field wiring'sresolve(), which rethrows the production refusal).service-settingsREADME: the file tier namesOS_HOME/~expansion, the project's legacy file and the two-file trial.one-shot-settings.ts: JSDoc thatresolveExistingDataKeysets or deletes both channels onopts.env, which defaults to the realprocess.env.content/docs/deployment/backup-restore.mdxright after the existing "A database backup withoutOS_SECRET_KEYis an incomplete backup" callout, the one page that already tells operators the home dir holds the dev-minted key: the two files, what the boot does when they differ, the split and refusal cases, and the remedy.content/docs/is devx-owned; this PR adds that one paragraph and nothing else there..changeset/30091-data-key-trial-page.md(patch), with a "Supersedes, in the same release" paragraph for cli: a boot whose data key cannot open the database's sys_secret rows still serves every secret as unset — os start / os dev should pick the key that opens them, or refuse loudly; one-shot commands read only the canonical key file #30042's pending note, which quotes the old refusal text.Tests
service-settingslocal-crypto-provider.test.ts(+8, describe#30091 — the trial scores both keys over the whole page, not its newest row): a split page ismixedon the majority key with an error naming both paths, in production and in development (no refusal); a tie goes to the key that opens the newest row, both orders; a corrupt newest row with older rows under one key settles on that key in production; one key opening every row isopenswith its tally; a page where nothing opens keeps the production refusal and recordsjudged;settleKeyFileChannelon a mixed page points the channel at the majority file and prints oneconsole.error, no warning. The reader-over-engine test now pins the whole page in order, the unknown marker still skipped. Existing trial tests moved to the page reader unchanged in what they assert.cliinternal-crypto-key-file-channel.test.tsandone-shot-settings.pin.test.ts: their stub readers answer a one-row page (the type change), assertions unchanged.Acceptance notes
chooseKeyFileByTrialOpenplus a(narrowing)declaration; it is raised in the dev report.mixedis aKeyFileChoicereason, and the refusal staysDataKeyOpensNothingError.Verification
All readings on head
26eb6119(branch merged withmainatff12aed3) unless named.Build, typecheck, tests
pnpm --filter '@objectstack/cli^...' --filter @objectstack/service-settings build, VERDICT command-exit 0; then@objectstack/clibuild, exit 0.@objectstack/service-settings0 (itstsconfigincludessrc/**, tests included),@objectstack/cli0 (withcheck:test-typecheck).service-settings: 1159/1159 (67 files), on26eb6119.cliunit (--project unit): 380/382 files on the first run, the two red files (published-subpath-console.pin,published-subpath-hook-body.pin) refusing becausepackages/cli/distwas not yet built; after the cli build both pass, 29/29. On26eb6119the two cli files this diff edits: 23/23. The cliintegrationlayer is declared to CI (no integration file or spawn entry is touched).Ablations (each through scripts/ablation-replace.mjs; restored blob == HEAD, `git diff HEAD` empty)
The pins live in
service-settings, whose tests import./local-crypto-provider.jsfromsrc, so no dist leg applies.page = (await opts.readStoredSecret()).slice(0, 1)): 8 red, the whole new#30091group.throw new DataKeyOpensNothingError(...)line deleted): 4 red acrosslocal-crypto-provider.test.tsandsettings-engine-bind-window.test.ts(fix(service-settings,cli): pick the data key file that opens the stored secrets, or refuse in production #30087's refusal pin and its channels-left-as-they-were pin, the new page refusal pin, and the real-kernel bind-window refusal).opens(if (rejected.opens === 0)forced true): 5 red (mixed ×2, tie ×2, settle-mixed).Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 95 derived families, the same list on the pre-merge head and on26eb6119. Each run with its exit code recorded before any pipe; on26eb6119all 95 exit 0, and--rananswers: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN. On the first pass three exited 3 for environment prerequisites (check:skill-examplesandcheck:dual-build-cjs-loadsbeforeclient-reactand seven other dists existed,check:i18n-coveragebeforeconnector-slack's); after building those packages all three exited 0.pnpm lint: exit 0 on26eb6119.Human Minimum
packages/specholds no runtime logic:packages/specuntouched.pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CahkbD22YHPM8hMrA8