Uh oh!
There was an error while loading. Please reload this page.
fix(spec): declare the eight-bullet credential read mask once, in spec (#7572) - #7759
Conversation
#7572) `SECRET_MASK` (objectql, the encrypted-FIELD read path) and `SETTINGS_SECRET_MASK` (service-settings, the settings REST read boundary) were two byte-identical literals bound by nothing. An edit to either would desynchronise the two masked reads a console sees, with both packages' suites still green — each asserted against its own copy. Hoist the mask into `@objectstack/spec` (`data/secret-mask.ts`, alongside the ADR-0100 surface in `data/field.zod.ts` / `data/object.zod.ts`); objectql re-exports it so its public API is unchanged, and service-settings aliases it as `SETTINGS_SECRET_MASK`. No objectql dependency was added to the settings service — spec is already in its runtime graph via platform-objects. Pins: byte + source-spelling pin at the declaration; an identity pin on each side that goes red if a local literal is re-introduced with different bytes. The far-side literal pins in plugin-audit / driver-memory keep their own copies on purpose.
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 111 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…ret-mask-single-source
Uh oh!
There was an error while loading. Please reload this page.
Rebase round for PR #7851 (#7521) after the domain:spec os-regen landing relay tail landed on main (#7758, #7759, #7763, #7782, #7813, #7892). Every `merge=os-regen` generated path is taken from origin/main WHOLESALE (`git checkout origin/main -- <paths>`, the 12 entries .gitattributes names), so this commit is a known-good base: the `os-regen` merge driver exits 0 without conflict markers while silently dropping one side, so a clean merge is indistinguishable from a dropped baseline. Regeneration lands as a SEPARATE commit on top of this one, and asserts the relay PRs' entries survived. Committed with --no-verify deliberately: the os-regen pre-commit hook wants the artifacts regenerated into this same commit, which would mix "what main brought" with "what my change produces" into one indistinguishable blob. The very next commit regenerates them, so the pushed tip is current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96
Fixes#7572
Option A from the card, per the 06:11Z triage note's restart condition ("the spec lane picks up Option A"): the eight-bullet credential read mask is now declared once, in
@objectstack/spec;objectqlre-exports it,service-settingsimports it.The invariant that was missing
The mask a client sees in place of a credential was declared twice, byte-identical by convention only —
SECRET_MASKin@objectstack/objectql(the encrypted-field read path, ADR-0100) andSETTINGS_SECRET_MASKin@objectstack/service-settings(the settings REST boundary, #7522). Nothing bound them, and the break would have been invisible from both sides: each package asserts against its own constant, so both suites stay green while the two surfaces disagree.That is worse than a cosmetic mismatch. A console renders "configured vs not configured" from this value and echoes it back unchanged on save; both write paths read that echo as "unchanged" (ADR-0100 §B3). A drifted mask silently turns an unchanged form round-trip into a real write of eight bullets over a live credential.
What changed
packages/spec/src/data/secret-mask.tsexportingSECRET_MASK, exported from the@objectstack/spec/dataentry point. Measured placement rather than a new home invented for it: ADR-0100's entire spec surface already lives insrc/data/(field.zod.ts'ssecret/passwordFieldTypes andackPlaintextMasking,object.zod.ts's author-time diagnostic). A dedicated leaf module beside them follows the existingdefault-value-tokens.tsshape — a reserved-string vocabulary declared once because two subsystems must agree on it.objectqlre-exports it fromsecret-fields.ts. Its public API is unchanged: same name, same value, same literal type, still exported from the package root and fromcore. No consumer edits anywhere.service-settingsaliases it asSETTINGS_SECRET_MASK, keeping the name that package publishes and every existing import working.The framework-agnostic property of the settings service is intact.#7522 declined to import the constant because reaching it meant depending on
@objectstack/objectql, the whole data engine. That reasoning was right and still holds — no objectql import was added here or anywhere in the package. It never applied to@objectstack/spec, which is already a dependency and already in this package's runtime graph:manifest.tsimportsSysSettingfrom@objectstack/platform-objects/system, whose object modules runtime-importObjectSchema/Fieldfrom@objectstack/spec/data. The marginal cost of the import is zero.Pins — judged one at a time
The hoist alone makes drift between the copies impossible; it cannot make an edit to the surviving declaration loud, and it cannot stop a local literal being re-introduced later. So per the card, each pin was judged on whether restating the literal was the point:
spec/src/data/secret-mask.test.ts(new).repeat(8), not an escape) so a grep for the bullets a client received finds the declaration.objectql/src/secret-fields.test.ts@objectstack/spec/data, and one restated eight-bullet case.service-settings/src/settings-secret-redaction.test.ts(new)plugin-audit,driver-memoryfar-side literalssettings-routes.test.tsroute-level assertionsReverse verification
Direction predicted before running, and both experiments matched.
1. Break the one literal (spec's declaration, eight bullets to seven; spec rebuilt so the consumers resolve it):
The two identity cases staying green is correct, not a hole: they compare one declaration with itself. The restated cases are the ones bearing load on this edit, and the ~30 green route/engine assertions are the measured demonstration of why the restatement exists.
2. Re-introduce a drifted local literal in service-settings (the pre-fix world):
settings-routes.test.ts.That second result is the card's claim, measured: before this PR nothing in the repo could see the drift.
Restored both times via
git checkoutagainst the checkpoint commit — nogit stash. The restore was verified by spec's build-input hash returning to its pre-experiment value (b1eb6b0a…) and by a cleangit statusplus 399/399 green.Changeset
One changeset, three packages, each graded separately:
@objectstack/spec— minor: a new public export (SECRET_MASKon thedataentry). Additive; nothing removed or renamed.api-surface/data.jsonandexport-origins/data.jsoneach gained exactly one line, which is the evidence for the grade.@objectstack/objectql— patch: no public API change.export-originsconfirms the re-export resolves to a single declaration, and the package's own export list is untouched — a consumer cannot tell the difference.@objectstack/service-settings— patch:SETTINGS_SECRET_MASKkeeps its name, value and literal type.Gates
Green locally:
@objectstack/specfull suite (378 files / 9885 tests) andtypecheck;objectqlsecret-fields.test.ts(21) andtypecheck;service-settingsfull suite (19 files / 399 tests).check:generated— all 13 artifacts current after regenerating the two the change moved.check:nul-bytes,check:merge-driver,check:adr-anchors,check:spec-parsed-alias,check:dual-source-exports,check:exported-anyall green.service-settingshas notypecheckscript; runningtscdirectly surfaces 5 pre-existing test-file errors (manifest i18n label typing,SettingsActionHandler) in files this PR does not touch — reported, not fixed.Generated by Claude Code