fix(cli): a one-shot command never mints a data key in the key home (#21471) - #21497
Conversation
…the fix Adds the one-shot settings composition (utils/one-shot-settings.ts) and the two pins that hold it: the enumeration of every CLI module that names the settings plugin or the local crypto provider, and, across every bootSchemaStack caller and mode, an empty key home in a development posture staying empty, with the default composition minting there as the control. The commands still compose the default here, so the pins are red on this commit by design; the next commit moves the commands onto the helper. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
`os secret orphans` and the storage arm of the data-migration plugins (`os storage orphans`, `os migrate files-to-references`) composed the settings service with no crypto provider, so it built its default one, which in a development posture with no key writes a key file into the key home. Every one of them now composes the service through utils/one-shot-settings.ts: the provider over a key that already exists, in the strict posture with the auto-key opt-in withheld, or one that refuses every call. `os secret rewrap` moves onto the same helper, so there is one spelling. The two driver-contract tests boot the commands' own composition again. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 27 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 bb30c025cd2dcd8706dad18f98c382c876137609 && git checkout bb30c025cd2dcd8706dad18f98c382c876137609
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9ff74285f14b7b60699546211e53e9ccc13c0d61 bda27b507349625ebc60538ceae017d8a1f7064e && git checkout -B drift-repro 9ff74285f14b7b60699546211e53e9ccc13c0d61 && git merge --no-ff bda27b507349625ebc60538ceae017d8a1f7064e
node scripts/docs-audit/affected-docs.mjs --json 9ff74285f14b7b60699546211e53e9ccc13c0d61
|
|
Landing note: one commit status is pending, and it is not a check run · On head
This diff touches no deployed app, so the seat readies and arms this PR and does not wait on the stuck vendor status. The merge queue runs its own Generated by Claude Code |
Fixes #21471
Clause-②: no
What changed
os secret orphansis a report that promises to write nothing. It composed the settings service with no crypto provider, so the service built its default one. In a development posture with no env key and no key file, that default creates a key file in the key home. The database stayed untouched, but the key home did not, and the next development-posture process on that host adopted the minted key. The storage arm of the data-migration plugins (os storage orphans, also report-only, andos migrate files-to-references) composed the service the same way.packages/cli/src/utils/one-shot-settings.tsholds the idiomos secret rewrapalready used, moved rather than copied:resolveExistingDataKey()builds the provider over a key that already exists, in the strict posture with the auto-key opt-in withheld, so it never mints;refusingCryptoProvider()refuses every call and names why;oneShotSettingsPlugin()hands the settings service one of those two, never the default.secret/orphans.tsandutils/data-migration-plugins.tscompose through it.secret/rewrap.tsmoves onto it, so there is one spelling, not two.rewrapsince its provider change.@objectstack/clipatch (.changeset/21471-one-shot-never-mints-key.md).Census: every CLI composition of the settings service, by any spelling
How the census was built:
packages/cli/srcwhose comment-masked code namesSettingsServicePlugin,LocalCryptoProvideror its deprecated alias (the enumeration pin's own detector; atda6acc015dit named exactly the four source composers below plus the new helper);os secret orphans(report and--delete)commands/secret/orphans.tsda6acc015d, green atbda27b5073os storage orphansutils/data-migration-plugins.tsda6acc015d, green atbda27b5073os migrate files-to-references(dry run and--apply)da6acc015d, green atbda27b5073. It needs the real key when one exists, because the storage plugin reads its stored credentials through the settings service. That is why the composition reads an existing key rather than always refusingos secret rewrapcommands/secret/rewrap.tsos serve, andos dev/os start, which spawn itsettingsrow; two default providers for secret fieldsos migrate plan/os migrate apply(composeHostStack)start()suppressed. The settings plugin builds its default from a hook registered instart(). Both are green in the family pinbootSchemaStackcallers (meta resync,migrateaccount-issuer/audit-metadata-bodies/duplicates/meta --stored/multi-value-columns/recorded-by/resume/summary-nulls/value-shapes)os verify@objectstack/verify's boot harness, in another packagePins
src/utils/one-shot-settings.pin.test.ts(unit tier)commands/serve.ts, failing by file name. A self-check confirms the detector ignores prose and sees a renamed destructure, the capability-table string and the alias.src/utils/schema-migrate.one-shot-family.integration.test.ts(integration tier, by its existingbootSchemaStackimport). It gains a third promise across the source-derived family: in a development posture with an empty key home, every mode of everybootSchemaStackcaller leaves the home empty.new SettingsServicePlugin({ registerRoutes: false }), the composition the report used to pass, leaves exactly one key file there.Verification (head
bda27b5073; red leg atda6acc015d)da6acc015d) and run against the unfixed commands:commands/secret/orphans.ts,commands/secret/rewrap.tsandutils/data-migration-plugins.ts(7 of 8 tests passed, the control included);migrate files-to-references,secret orphans,storage orphans,migrate files-to-references --applyandsecret orphans --delete, each as "key material was created in the key home", with the key file present.bda27b5073.vitest run src/utils/one-shot-settings.pin.test.ts: 8 / 8 passed.vitest run --project integrationover the family file and both driver-contract files: 3 files, 85 / 85 passed.orphans.guards,rewrap.guards,summary-nulls,sys-secret-rewrap), 4 files, 37 passed.pnpm --filter @objectstack/cli typecheck(tsc pluscheck:test-typecheck): exit 0, with no new test-typecheck debt.os secret orphans --json, in a development posture with an empty key home, left the home empty.pnpm lint(eslint . --no-inline-config, the whole repo): exit 0 atbda27b5073, not narrowed.dispatch-gates.mjs --ranwith no paths: 64 derived, 64 run, every one exit 0, and 0 NOT MEASURED (a derived zero, from recorded exit codes). Four gates first refused on a missing build (exit 3, nothing measured). After the prerequisite builds they were re-run to exit 0:check:dual-build-cjs-loads,check:i18n,check:i18n-coverageandcheck:i18n-walk-parity.origin/main. It is three commits ahead, and none of them touches these files. The derivation's stale-tree note namesscripts/engine-double-contract.pinned.json, which this diff does not touch.Acceptance notes
os secret rewrapalready did. stdout and--jsonare unchanged.nullwith a warning. A freshly minted key produced the same, because it can open nothing stored.packages/cli/src. A composition inside another package that a command calls into names nothing there; the pin header says so, and the census lists the one that exists.Out of scope (reported to the seat, not filed here)
os verifyreaches@objectstack/verify's boot harness. The harness composes the settings plugin with no provider and also sets a default local provider on the engine for secret fields.examples/app-todo, in a development posture with an empty key home: after the run, the key home held a key file.packages/cli. And it needs a different provider shape: the harness seals and opens secret fields against an in-memory database, so a read-or-refuse provider would breakos verifyon a keyless host where an ephemeral in-process key would not.Generated by Claude Code