Repository navigation
fix(service-settings): the settings audit trail records secret-valued settings with the keyed digest - #21809
Conversation
… settings with the provider's keyed digest Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 140 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 425deaaa0db9de89126accf04e6314a3d9a33480 && git checkout 425deaaa0db9de89126accf04e6314a3d9a33480
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 75ddcd1b41e74823b7bb9f3fe9e159a189fa2a91 7f7487c65bf97e2ddf3d7a2e8612c5c3de53c6a6 && git checkout -B drift-repro 75ddcd1b41e74823b7bb9f3fe9e159a189fa2a91 && git merge --no-ff 7f7487c65bf97e2ddf3d7a2e8612c5c3de53c6a6
node scripts/docs-audit/affected-docs.mjs --json 75ddcd1b41e74823b7bb9f3fe9e159a189fa2a91
|
…secret-valued keys Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
…ui#11636, objectstack-ai#11637, objectstack-ai#11635, objectstack-ai#11624 and objectstack-ai#11640) (objectstack-ai#21827) Fixes objectstack-ai#21807 Clause-②: no This moves the bundled Console's objectui pin from `9dfaca654311` (objectstack-ai#21772, PR objectstack-ai#21800) to `0abd4f9f8769fc4c19ad2f96707684876f74c09f`, which was objectui `main` at write time and is past `39a3e91fad`. The Console now carries objectui#11636, the fix for objectui#11092: the screen-flow runner names the flow by its served label, translated. That merge is what objectstack-ai#20318 waits on. The range also carries objectui#11637 (objectui#11170), objectui#11635 (objectui#11095), objectui#11624 (objectui#11396) and objectui#11640 (objectui#11628). ⛔ This is not a release act. Version PR objectstack-ai#21352 is untouched, and no tag, publish or Release was made. ## Range - objectui `git ls-remote origin refs/heads/main` read `0abd4f9f8769fc4c19ad2f96707684876f74c09f` at 2026-10-05T04:53:29Z, just before the bump. - Re-read at 05:37:53Z, it reads `59917c4b2` (objectui#11639, a served view's toolbar change written inside `config`), one commit past the pin. This PR does not carry it. - The objectui clone is full (`--is-shallow-repository`: false). `git merge-base --is-ancestor` exits 0 against `0abd4f9f8` for `39a3e91fa`, `c4c506b9e`, `22ddcd5c5` and `1c2e2c46c`. - `9dfaca654311..0abd4f9f8769` has 5 commits and 0 merges. - objectui declared 5 changesets over the range: 3 release and 2 release nothing. Every commit carries a changeset. None declares `major`, and none carries the breaking annotation. The highest declared level is `minor`, so the console changeset is `minor`. These counts come from the bump's own digest and were re-read from the changeset blobs. - **No commit subject in the range carries `!`.** `git log --format='%h %s' 9dfaca654..0abd4f9f8 | grep -E '^[0-9a-f]+ [a-z]+(\([^)]*\))?!:'` matches nothing (exit 1). | objectui commit | landing | changeset | note | |---|---|---|---| | `1c2e2c46c` | objectui#11624 (objectui#11396): `MasterDetailDetailConfig` is derived from the spec's `details` entry by reference, member for member the same | release-nothing | moves the manifest (below) | | `22ddcd5c5` | objectui#11635 (objectui#11095): a dataset-bound KPI tile's re-read on the data-invalidation bus gets a pin; tests and a doc comment only | release-nothing | | | `39a3e91fa` | objectui#11636 (objectui#11092): the flow runner names the flow by `flows.FLOW.label`, then the served `flowLabel`, then the API name | minor (`Clause-②: yes (widening)`) | smoke below; unlocks objectstack-ai#20318 | | `c4c506b9e` | objectui#11637 (objectui#11170): one declaration of per-type NODE SLOTS; `objectui check`, core `validateSchema`, the SDUI parser's `validateTree` and the `kind:'html'` compile walk them | minor (`Clause-②: yes (narrowing)`) | answered below | | `0abd4f9f8` | objectui#11640 (objectui#11628): the External Datasource panel unwraps the `{ success, data }` envelope | patch | smoke below | ## Declared-breaking entries and the ADR-0087 disposition **There are none to dispose of.** No commit subject carries `!`, no changeset declares `major` or the breaking annotation, and the bump wrote no `adr-0087: TODO` placeholder. `check-adr-0087-registration --base origin/main` reports "this PR adds no declared-breaking changeset". `check-changeset-no-major --base origin/main` reports "This diff introduces no `major` bump". One entry narrows by its own declaration, so here is how this repo answers it. **objectui#11170 (`c4c506b9e`, `Clause-②: yes (narrowing)`)** widens the reach of four objectui validators: each now judges nodes held in a renderer's declared slots, not only in `children`. It adds, removes or renames no ObjectStack-authorable key, and no Zod schema or stored `sys_metadata` shape moves. Its sdui-parser half only takes effect when a manifest is built with `slotsFor`. This repo's `scripts/gen-sdui-manifest-node.mjs` calls `manifestFromConfigs(configs)` without that option, and the regenerated `sdui.manifest.json` carries 0 `slots` keys. So the save gate's walk does not move with this bump. The lockstep reading is in Acceptance notes. ## What changed here (22 files, +255 / -93) - **`.objectui-sha` and `.changeset/console-0abd4f9f8769.md`.** `scripts/bump-objectui.sh 0abd4f9f8769fc4c19ad2f96707684876f74c09f --no-commit` wrote both, with `OBJECTUI_ROOT` set to a read-only objectui clone in the scratchpad. The range walked completely and the level was auto-set to `minor`. - The digest rendered objectui#11170's bullet as its first line, the bare `**Clause-②: yes (narrowing)**`. That bullet is rewritten to say what landed. - A closing paragraph states the range has no declared-breaking entry and names the release-nothing pair. - **`sdui.manifest.json` and `scripts/sdui-manifest.record.json`.** `node scripts/gen-sdui-manifest-node.mjs` regenerated them over the tree that `pnpm objectui:build` built at the pin. It still has 107 components, and the sha256 moves from `6f921896ffac…` to `f95d406a584e…`. - One input moved: `object-master-detail-form`'s `details` gains `of: "object"` and a description of the spec's closed entry (objectui#11396). `"of": "object"` occurrences go from 12 to 13. - The record moves its pin and `modulesRoot`. objectui's workspace version stays 17.7.0, confirmed against the pin by `check-sdui-manifest --require-objectui`. - **`packages/sdui-parser/objectui-lockstep.json`.** `gen:sdui-lockstep` re-recorded it from the clone at the pin. It records 214 grammar lines (blob `0131f27cf86d`), 25 diagnostic codes and containment predicate `76c18fb95d1f`, all unchanged, so no port is owed by that gate. - **The 54 asserting pin citations in `packages/spec/src`, re-measured, not restamped.** - **Method.** The range changes 50 paths. Each asserting record's cited objectui paths were resolved against the tree at `9dfaca654`. Ambiguous bare names were disambiguated by the record's own directory. Each `index.tsx` and `types.ts` cited is a `plugin-kanban`, `plugin-dashboard`, `plugin-map`, `plugin-gantt`, `plugin-grid`, `plugin-tree` or `plugin-timeline` file. None is `plugin-form/src/index.tsx` or `sdui-parser/src/types.ts`, the two same-named files the range touches. - **Result.** No asserting record cites a changed path. So every cited file is byte-identical across the hop, and every anchor held unmoved. - **Records.** Each of the 41 hand-written records gains a dated 2026-10-05 hop sentence and keeps its earlier history. - **Counts.** Three records carry a count, and each was re-taken by its own method; all three read the same at `2e818d0b5`, `9dfaca654` and `0abd4f9f8`: - the `keyboardNavigation` hit lines: 15, against 3 for the `schema.editable` control; - `ObjectKanban.tsx`'s `quickAdd` / `onQuickAdd`: 2 each, against 11 for `onCardClick`; - the `ElementDataSourceGate` occurrences in the five `src/index.tsx` shells: 0, 3, 3, 3 and 4. - **Corpus counts.** The six migration entries' counts were re-taken with `git grep -o -F`. That method first reproduced every `9dfaca654` number: 7632 files, `objectstack` 17313, `@objectstack/spec` 7186, `timeout` 1360, `useState` 2477, `TTL` 182, `tenant` 1318, `RuntimeConfig` 293, `resourceLimits` 2, `window` 4193, `period` 238, `interval` 195, `metrics` 401 and `Span` 508. - The new readings are 7650 files, `objectstack` 17390, `@objectstack/spec` 7209 and `useState` 2478. `window` reads 4194. Every other control is unchanged. - All 98 checked tokens (the export lists of `plugin-lifecycle-advanced.zod.ts`, `tracing.zod.ts` and `metrics.zod.ts`, plus every named key) read the same at both pins: every zero is still zero, and `Span` / `SpanSchema` read 508 / 57. - `packages/spec/src/migrations/registry.ts` was regenerated with `gen:migration-registry`. - **`.changeset/objectui-pin-citations-0abd4f9f8769.md`** is a `@objectstack/spec` patch, because the `FormField.span` describe and six migration descriptions name the pin. `content/docs/references/ui/view.mdx` was regenerated by `check:generated --fix`. No example, test or gate needed adapting, and no code changed outside generated records, citations and changesets. ## Console build (the local Console Pin Gate equivalent) `turbo run build --filter=@objectstack/client... --filter=@objectstack/spec...` and then `pnpm objectui:build` ran as one command under the verify lock. That is a clean build: mode 3 shallow-cloned objectui at the pin into `.cache/objectui-0abd4f9f8769`. Exit 0, 10m51s on the shared box. The build log reports: - "Bundle canary 'import/jobs' present". - "Single-zod canary: exactly one zod version literal {major:4,minor:6,patch:5}". - **"Console bundle carries THIS tree's @objectstack/spec, and only it"**: the spec-injection check passes. - "@objectstack/console dist ready (64232 KB) from objectui@0abd4f9f8769". `check:console-sha` and `check:console-injection` (self-test 67 assertions, then the dist check) exit 0 against that dist. ## Browser smoke: `examples/app-showcase` with the Console built at `0abd4f9f8769` The server ran `pnpm dev -- --fresh --ui --no-watch -p 41907` with `OS_PORT=41907`, on its own ephemeral DB with the seeded admin. Headless Chromium (`/opt/pw-browsers/chromium`) drove it, signing in through the console's login form. Page errors, console errors and every 4xx/5xx response were captured. Only the PIDs this run started were stopped. **Flow label (objectui#11636 / objectui#11092).** The launch was Tasks list, select a row, then the toolbar's "Reassign…" (`showcase_bulk_reassign`, which targets the screen flow `showcase_reassign_wizard`, label "Reassign Task"). | leg | trigger answer | runner header line | dialog title (accessible name) | API name shown | completion toast | |---|---|---|---|---|---| | `en` | 200, `status: paused`, `flowLabel: "Reassign Task"` | `Reassign Task` | `New Assignee` (the screen's own title) | no | `Flow "Reassign Task" completed` | | `zh-CN`, with a bundle entry `flows.showcase_reassign_wizard.label` | 200, `flowLabel: "Reassign Task"` | `重新分配任务` | `New Assignee` | no | `流程「重新分配任务」已完成` | - The resume answered 200 with `flowLabel` on both legs. - **The showcase bundle declares no `flows` translations** (`git grep` finds none), so the `zh-CN` leg needed one to exist. It was made by a temporary local edit, as objectstack-ai#21800's smoke did for `imageField`: - `node scripts/ablation-replace.mjs --hold` added `flows: { showcase_reassign_wizard: { label: '重新分配任务' } }` to the `zh-CN` block of `examples/app-showcase/src/system/translations/index.ts`. - The server booted with `--compile`. - The file was restored at once with `--restore`. The tool reports the blob back to HEAD's `f0517049b565` and `git diff HEAD` empty, and `git status --porcelain` is empty. - `GET /api/v1/i18n/translations/zh-CN` served the entry, and the session's `html[lang]` read `zh-CN`. - The showcase `dist/` was rebuilt afterwards (`turbo run build --filter=@objectstack/example-showcase --force`), and the held string has 0 hits in `dist/objectstack.json`. Nothing of the edit is in this diff. - **Verdict:** at this pin the runner header and the completion toast name the flow by the active language's `flows.FLOW.label`, then the served label, and never by the API name. The launcher button still reads the ACTION's label ("Reassign…"), which is the action's own string and not the flow's. **The range's other landings.** | landing | surface | observed | |---|---|---| | objectui#11640 (objectui#11628) | Setup → `metadata/datasource/showcase_external` | The External Datasource panel lists the remote tables `customers` (7 columns) and `orders` (7), each with Import. "Refresh catalog" (`POST …/external/refresh-catalog` 200) shows `snapshot 10/5/2026, 5:25:23 AM`. "Run validation" (`POST …/external/validate` 200) renders "All 2 objects match the remote schema." with `showcase_ext_customer` and `showcase_ext_order`, and no render error. | | objectui#11624 (objectui#11396) | `page/showcase_project_workspace` (`object-master-detail-form`, `details: [{ title: 'Tasks', childObject: 'showcase_task', addLabel: 'Add task' }]`) | The master form draws its 6 fields. The Tasks section draws, and "Add task" opens the child's inline form with 14 more fields (Title*, Assignee, Priority, …). 0 page errors. | | objectui#11637 (objectui#11170) | the three `kind:'html'` pages: `showcase_start_here`, `showcase_capability_map`, `showcase_command_center_jsx` | All three render (1563 / 2426 / 882 characters of text), with no compile or validation text and 0 page errors. | | objectui#11635 (objectui#11095) | `dashboard/showcase_ops_dashboard`, `showcase_revenue_pulse`, `showcase_chart_gallery` | The ops tiles read Active Projects 2, At-Risk (Red) 1, Awaiting Review 2 and Total Budget 1,090,000, matching objectstack-ai#21710's REST cross-check. All three dashboards have 0 page errors. This landing changes no executable code (its changeset: tests and one doc comment). An out-of-band REST `PATCH` of a task to `in_review` did not re-read the open tile: it stayed 2 with 0 dataset queries in 6s, and read 3 after a reload. That mutation never travels the console's own invalidation bus, so this is no reading of objectui#11095's pin. **NOT MEASURED** there. | **Console messages across the run:** 0 page errors. The failed loads are the pre-login `401 GET /api/v1/auth/get-session`, one `404` per page load that the response listener did not attribute (`/favicon.ico` answers 404 on this server, as objectstack-ai#21800 recorded), and `404 GET /api/v1/meta/datasource/showcase_external?state=draft`, the panel's draft probe for a datasource that has no draft. ## Gates and tests (head `e40526e564`) - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 127 commands from the 22-path diff, and all 127 were run at `e40526e564` and exited 0. `--ran` reports "127 derived, 127 run, 0 NOT-MEASURED, 0 UNRUN", with every exit code recorded. The full workspace build (`turbo run build --filter=!@objectstack/docs`) ran first, so no dist-reading gate met a missing prerequisite. - Also exit 0: - `check:objectui-pin-citations --verify-anchors` with `OBJECTUI_ROOT` at the pin: "54 asserting objectui pin citation(s) match .objectui-sha (0abd4f9f8)", and "7 anchor content assertion(s) verified against objectui at 0abd4f9f8". - `check:objectui-bump` (20 assertions across 5 cases), `check:sdui-lockstep`, `check-sdui-manifest --require-objectui`, `check:console-sha`, `check:console-injection`. - `check-adr-0087-registration --base origin/main` and `check-changeset-no-major --base origin/main`. - `@objectstack/spec check:generated`: all 15 artifacts current after the `--fix`. - `pnpm --filter @objectstack/spec exec vitest run`: 668 files, 19261 passed, 1 todo. `pnpm --filter @objectstack/spec typecheck`: exit 0. - `@objectstack/sdui-parser` test (14 files, 225 passed) and typecheck: exit 0. - These suites read the regenerated manifest: - `@objectstack/lint` (119 files, 5627 passed); - `@objectstack/metadata-protocol` `src/protocol.runtime-authoring-gate.test.ts` (70 passed); - `@objectstack/cli` unit tier `src/utils/sdui-manifest.test.ts` and `test/validate-build-gate-parity.test.ts` (2 files, 79 passed). - The CLI integration tier is declared to CI. - `eslint --no-inline-config --format json` on the 15 changed TS files: 15 files, 0 errors and 0 warnings. - The lint population is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (`eslint.config.mjs:971`). - The config enables no type-aware linting (`:328`), so this diff cannot move a verdict on an untouched file. - Repo-wide `pnpm lint` is left to CI. ## Acceptance notes - **Lockstep, a reading and not a gate result.** objectui#11170 gives objectui's `validateTree` a slot walk: `slotElements` plus the `comp?.slots` loop in `packages/sdui-parser/src/validate.ts`. This repo's port has none (`git grep -n slots packages/sdui-parser/src`: 0). - The walk only fires for a manifest built with `slotsFor`, and this repo's committed manifest carries no `slots`. So the save gate and objectui's runtime parser still agree on that manifest. - objectui's `kind:'html'` compile (`getJsxManifest`) is now built with `slotsFor`. A slot-held node that fails validation could fail the renderer's compile while this repo's save gate accepts it. - `check:sdui-lockstep` compares the grammar region, the codes and the containment predicate, so it cannot see a walk-depth change. That class is outside its reach by its own header ("CANNOT see … WHEN a code fires" beyond the predicate). - Not measured through a public door here, so it is noted rather than filed. Carrier: none. - `main` moved three commits past this branch's base (objectstack-ai#21810, objectstack-ai#21808, objectstack-ai#21809). `git merge-tree --write-tree HEAD origin/main` is clean. Two of them touch files this diff touches: `api-methods-batch-conformance.test.ts` (test titles) and `component.test.ts` (objectstack-ai#21808 re-points a non-asserting anchor). The merged tree still carries 1 and 6 citations at the new pin and 0 at the old one. No merge was made; the merge queue rebuilds on the current `main`. - objectui `main` reached `59917c4b2` (objectui#11639) after the pin was read. It is not in this range. - Writes: one draft PR through the relay and the report comment on objectstack-ai#21807. No label, PR assignee, ready flag or auto-merge was written; this dispatch's write budget names none of them. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…jectstack-ai#21943) Part of objectstack-ai#21932 Clause-②: no ## What changes The platform checklist gains items for the rules the 17.7 pre-release security follow-up landed, and two re-checks from the card are resolved. All edits are in `docs/qa/platform-checklist/areas/*.json`. `automation.json` is untouched (open PR objectstack-ai#21928 holds it). | Card row | Disposition | Item | |---|---|---| | objectstack-ai#21792 (PR objectstack-ai#21809) settings audit and secret-valued settings | new item | `platform-core.settings-audit-secret-fingerprint` | | objectstack-ai#21846 (PR objectstack-ai#21872) implicit account linking | new item | `identity-auth.implicit-account-linking-ownership` | | objectstack-ai#21839 (PR objectstack-ai#21890) share-link password | three clauses added, rev 4 to 5 | `access-security.share-link-capability-tokens` | | objectstack-ai#21836 (PR objectstack-ai#21879) global search skips unreadable objects, plus the two cases objectstack-ai#21880 lists | new item | `search.global-search-skips-unreadable` | | re-check 1: A2 / A7 and the plugin-driver boundary | rev 2 to 3 | `integration-system.datasource-credential-refusal-matrix` | | re-check 2: the objectstack-ai#21845 CLI and quorum N1 notes | already applied by objectstack-ai#21891, no edit | `cli.scaffold-first-run`, `cli.scaffold-console-first-paint`, `approvals.quorum-m-of-n` | Each item states rules, not reproductions. Withheld security detail stays out. ### Grounding, per row - **Settings audit fingerprint.** Both ledgers record the keyed digest for a secret-valued setting, or no fingerprint when none is available, and never the value or an unkeyed hash. Grounded in `settings-service.ts#secretAuditDigest`, `config-change-audit.ts#CONFIG_CHANGE_ACTION` and the contract text at `crypto-provider.ts#keyedDigest`. The pin is `settings-audit-secret-digest.test.ts` (7 cases). The offline check carries a positive control: the non-secret key's unkeyed digest IS found, so a no-hit on the secret rows means something. The no-keyed-digest arm cannot be reached on a stock boot, so that clause is scored from the pin. - **Implicit account linking.** Four rules: no implicit link to an unverified local user; an unlink is honoured; an explicit, signed-in link still works and lifts the refusal; the platform IdP exception holds only on its OAuth path. Grounded in `implicit-account-linking.ts` (`decideImplicitLink`, `IMPLICIT_LINK_REFUSED`, `PLATFORM_IDP_PROVIDER_ID`, `recordUnlinkTombstone`, `refuseImplicitAccountLink`) and the published `sso.mdx` section. The pin is `implicit-account-linking.test.ts`. The item reuses the local OIDC provider recipe from `identity-auth.linked-accounts-social`. The platform-IdP clause and the operator override are pin-scored, and knownGaps says why. - **Share-link password.** The stored hash leaves on no exit (mint, list, redemption). The password is accepted from the `X-Share-Password` header, the query form is still accepted, and the default CORS allow-list carries the header. Both public routes answer `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, and the authenticated routes do not. Grounded in `share-link-service.ts#withoutPasswordHash`, `share-link-routes.ts#SHARE_LINK_PUBLIC_RESPONSE_HEADERS`, the runtime `share-links.ts#PUBLIC_RESPONSE_HEADERS` and `adapter.ts#DEFAULT_CORS_ALLOW_HEADERS`. The pins are the `[objectstack-ai#21839]` blocks in `share-link-password.test.ts`, `share-links-public-cache-headers.test.ts` and the hono-plugin CORS case. Existing clause indices are unchanged. - **Global search.** An unreadable object is never queried, named or counted. An explicit `objects=` naming one answers exactly as a name that matches no object. The object stays refused at its own door. Row scope still narrows a searched object, and a term found only in a field hidden from the caller yields no hit. Grounded in `protocol.ts#searchAll` (the `canReadObject` pre-filter and the `getQueryableFields` narrowing). The pins are the dogfood `search-skip-unreadable.dogfood.test.ts` and the 12 unit cases in `protocol.search-skip-unreadable.test.ts`. The two objectstack-ai#21880 cases have no end-to-end pin yet, and knownGaps says so. The open pinyin-companion finding on objectstack-ai#21880 is recorded as a knownGap with a flag-off instruction, at class level only. The persona reuses the area recipe `qa-contributor-bound-member`. - **Datasource credential matrix.** A2 / A7 (`acceptance[1]` and `acceptance[6]`) are recorded as a known environment gap. They need a reachable credential-protected database of a shipped driver, which no run has had. No recipe is claimed, because none is proven. A successful publish alone may not score them, and the stored-credential half of A7 can be read as a partial reading. Separately, the unknown-driver clause, step 7, its negative and the title now state the ruled boundary from objectstack-ai#21921 and the docs note objectstack-ai#21927. For a plugin driver, only the fixed spellings are redacted (the canonical keys, the former aliases and URL credentials). A non-canonical key served as written is the boundary, not a FAIL. Grounded in `common.zod.ts#CANONICAL_CREDENTIAL_KEYS` and `datasource-credential-redaction.ts#redactableConfigKeys`. ### Re-check 2 evidence (no edit) At the claim ref `9dce635337`: - `cli.scaffold-first-run` (rev 3) step 0 and `cli.scaffold-console-first-paint` (rev 3) step 0 both drop the trailing `npm install` and warn against adding it. Their rev 3 history entries cite objectstack-ai#21845. No other `npm install` step remains in `cli.json`. - `approvals.quorum-m-of-n` (rev 4) `negative[0]` requires a NON-PRIVILEGED repeat actor and names the documented admin override (objectstack-ai#3424) as never a distinctness FAIL. ## Remaining on objectstack-ai#21932 (held, not in this PR) - The objectstack-ai#21864 row (public-form withdrawal layering). Its PR is still open. - The objectstack-ai#21928 row (run-state trigger record mask). That PR adds its own item in `automation.json`. objectstack-ai#21932 remains open for these two rows. ## Validation (at `a72b827e43`) - `pnpm check:platform-checklist`: exit 0. It reports 15 areas and 273 items (269 active, 2 planned). The baseline was 270. Symbol anchors resolve 674 of 684 (baseline 657 of 667): all 17 new anchors resolve, and the objectstack-ai#16898 residual is unchanged at 10. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 13 commands, and all 13 exit 0. `check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` were not built). After building them it exited 0. `--ran` reconciliation: 13 derived, 13 run, 0 unrun. - No package source changed, so there is no package build, test or typecheck. No changeset: `docs/qa/**` publishes nothing. ## Acceptance notes - Source citations name test cases and symbols, never line numbers, because `check:platform-checklist` refuses a `file:line` pin. - `content/docs/data-modeling/drivers.mdx` says a plugin driver's `config` is "stored and served to administrators as written". The read redactor still withholds the canonical spellings (`password`, `authToken`), the former aliases and URL credentials for such a driver (`redactableConfigKeys`). So the docs sentence is slightly broader than the code, and the code is the more protective of the two. The checklist follows the code. This is noted only, with no card. Carrier: none. - A run of `search.global-search-skips-unreadable` picks the walled object and the hidden-field value on the live boot, behind premise guards. The item names likely candidates and does not assume them. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21792
Clause-②: no
What changes
The settings audit trail now records secret-valued settings (encrypted keys) with the platform's keyed digest (
ICryptoProvider.keyedDigest), never the unkeyeddigest. This follows the maintainer ruling recorded on the card. Non-secret settings are unchanged.packages/services/service-settings/src/settings-service.ts: one private helper,secretAuditDigest, supplies the fingerprint for a secret-valued write on both write branches (thesys_secretpath and the legacy inline-adapter path). Both ledgers take it: thesys_audit_logconfig_changerow (valueDigest, now spelled with anhmac-sha256:digest inside the existing encrypted marker) and thesys_setting_auditrow (newHash).CryptoAdapter) or when the provider refuses. The rows then record the write with no fingerprint:valueDigestis the bare encrypted marker andnewHashis null. Awarnis logged once per key. The write itself is never refused, matching the existing rule that a ledger never fails a settings save. The unkeyed digest is never used as a fallback.packages/spec/src/contracts/crypto-provider.ts: the contract text is revised as the ruling asks.digestis the audit fingerprint of non-secret values only.keyedDigestrequirement 3 now says a secret's audit fingerprint comes from it and never fromdigest, and that with no keyed digest the trail records none. Comment-only: no type, export or schema change.@objectstack/service-settingspatch and@objectstack/specpatch (the doc text ships in the.d.ts).Consumers that compare these digests
No non-test code reads or compares
valueDigest/new_hash/old_hash:git grepoverpackages/**returned only the writers, the object definition and comments. Equal-value detection still works for a reader, because the keyed digest is stable for equal input under one key. Three existing tests pinned the old unkeyed spelling for a secret. They were updated, not deleted: two now expect the keyed shape, and one (the legacy adapter, no provider) now expects no fingerprint.Tests (at
f34f69594c; test code is unchanged sincefc5c86e1c6)settings-audit-secret-digest.test.ts(7 cases). For a secret, both ledgers carryprovider.keyedDigest(value), which is neitherprovider.digest(value)nor the plain SHA-256 of the value. The fingerprint is stable for equal values and distinct for different ones. A reset records null. The legacy branch with a provider records the keyed digest. The legacy branch with no provider records no fingerprint, warns once, and the write still lands. A provider that refuses still lets the write land. For a non-secret value, both ledgers keep the adapter digest of the canonical JSON, unchanged.pnpm --filter @objectstack/service-settings test: 34 files, 612 tests passed.typecheck(tsc --noEmit,include: ["src"], so it covers the tests) passed.scripts/ablation-replace.mjs. The tests importsrc, so no dist rebuild was involved. Leg 1 put back the unkeyeddigeston thesys_secretbranch: 2 of 7 failed (the keyed-digest case and the refusing-provider case). Leg 2 put back the adapter digest on the legacy branch: 2 of 7 failed (both legacy cases). Each leg restored to the HEAD blob (a148f503e350) withgit diff HEADempty.dispatch-gates.mjs --commands, 87): 85 exit 0.check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: it needs a whole-repo build).check:type-check-debtwas killed twice by the foreground cap. Both are NOT MEASURED and left to CI.--ranreconciliation: 87 accounted, 0 unrun..tsfiles:eslint --no-inline-config --format jsonreported 5 files, 0 errors, 0 warnings. The config's TS block matches**/*.{ts,…}, and type-aware linting is not enabled (noparserOptions.project), so this diff cannot change any verdict on an untouched file.Acceptance notes
oldHash(not done). Thesys_setting_auditwriter still hard-codesoldHash: null. Recording the previous fingerprint of a secret would mean decrypting the prior ciphertext on every write, so it is not the small rider the dispatch allowed. It is left for a follow-up.sys_setting_auditobject'sold_hash/new_hashfield descriptions inplatform-objectsstill say "SHA-256 of the … value". That was already inexact for the default adapter, and it is outside this card's file surface. Carrier: none named.Generated by Claude Code