Repository navigation
fix(service-settings)!: a settings value that misses its type's declared format is refused at the save door and the env door - #25022
Conversation
…at its type declares, at both doors The save door answers one FieldError per key with the code the format verdict carries (invalid_email / invalid_url / invalid_phone / invalid_format) through the existing SETTINGS_VALIDATION envelope, and stores nothing. The env door rejects a malformed OS_* override through reportRejectedEnvOverride: not in force, reported once, never printing the value. The pin file is re-pointed from warning to refusal. Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…ngeset for service-settings The warning changeset described behaviour this branch changes before any release consumed it, so it is replaced by one BREAKING minor changeset that states the save door and the env door refusal, the env fallback and what an operator does. Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
… refusal pin Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…ttings-format-refusal
📓 Docs Drift CheckThis PR changes 1 package(s): 3 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 — 9 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 6560b87021b940c8463f6424743c5db541508571 && git checkout 6560b87021b940c8463f6424743c5db541508571
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 05b28c199b7408da7ca4bc3988635fa9342cd6de 58d0be84ffd522783c97313570a02e0971989ee9 && git checkout -B drift-repro 05b28c199b7408da7ca4bc3988635fa9342cd6de && git merge --no-ff 58d0be84ffd522783c97313570a02e0971989ee9
node scripts/docs-audit/affected-docs.mjs --json 05b28c199b7408da7ca4bc3988635fa9342cd6de
|
Contract reviewServed-tier: Inputs: card #24882 (body and all nine comments), PR #25022 (body, file list, the net diff against main at the head above, five paths), and the check-runs on that head. Read-only: the cited code was read with git show at the head and at origin/main; nothing was built, run or re-run. ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong.
①-a The deliberate correction:
|
席位核对ACCEPT · #24882 (the refusal) ·
|
Fixes #24882
Clause-②: no (narrowing)
This PR lands the card's last half: both settings doors now refuse a value that misses the format its type declares. PR #25001 (
91e728669) landed the warning half. The console half is objectstack-ai/objectui#13287.What changes
ADR-0007 declares a format for four specifier types:
email,urlandphonetake "format validation", andcolortakes a hex string. PR #25001 added one shared verdict,firstFormatMiss(type, value), which already carries the ADR-0114 code for each type. This PR changes its two call sites inpackages/services/service-settings/src/settings-service.tsfrom warning to refusal. The grammars are unchanged.validatePatch). A miss pushes oneFieldErrorfor the key, with the verdict's code (invalid_email,invalid_url,invalid_phone, orinvalid_formatforcolor), andcontinues like every other family. The route answers through the existingSETTINGS_VALIDATIONenvelope:400, with the entry inerror.details.fields. Nothing in the save is stored. The format is still judged last, so a value that also breaks another family (a length bound, a pattern) is refused for that family.Logo URL must be a well-formed URL (scheme://…, a root-relative /path, a data: URI or a blob: URL).It never echoes the value, so the entry carries novaluemember.constraintis{ type: 'url' }and so on. This is the existingFieldError.constraintmember, keyed by the declaring property as the pattern branch's{ pattern }is. It lets a client tell a colour'sinvalid_formatfrom a declared pattern's.effectiveEnvOverride). A miss is rejected throughreportRejectedEnvOverride, the same reporter every other family uses. The override is not in force, the line is logged once per (variable, value) aterror, and the key is no longer locked. The reporter gains one optional fragment,shape. When it is set, the line carries the value's length and never the value, for any key, not only an encrypted one.auditEnvOverridesstill walks format-typed keys, so the line arrives at boot.reportEnvFormatMiss, its dedupe set, the save door'sformatMisseslist and its post-acceptwarnloop.No response member is added, and nothing outside
service-settingschanges. No organization predicate or rung resolution is touched.「甲」 reading (amended)
The standing rule 「甲」 (#6015, comment 6292005854), as the maintainer amended it (comment 6303865025, quoting 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」):
The seat's derivation for this card (comment 6302777692) still holds on its facts:
sys_setting, and anOS_*override is a deployment's environment.Under the amended rule 2, that narrowing ships now. The seat re-timed the card on that reading (comment 6304784014). So this PR is a BREAKING
minor:Clause-②: no (narrowing);not-required (no-migration-prescription), accepted bycheck-adr-0087-registration --base origin/main;CONTRACT_REVIEW_TIERbefore the queue (claim comment 6304887064).Measured first
Stored rows. A value already stored that misses its format was measured on every shipped format-typed key and on the synthetic
phonekey. Each was stored astext, then the real manifest was registered over it, the method the option-table and value-domain TOUCH pins use.get()andGET /api/settings/:namespacereturn it unchanged. Reads judge nothing. This was expected.branding.workspace_namesaves over a stored malformedlogo_url, andmail.from_namesaves over a storedno-reply@localhost. This was expected: the save door judges only the keys a save touches.validatePatch's TOUCH gate also counts a key whosevisiblepredicate reads a patched key. A syntheticurlkey gated onmodeis judged whenmodeis saved and it stays visible. That rule is the same for every family. No shipped format-typed key declaresvisible, so no shipped namespace is affected. It is pinned so the reading is exact.The env door's fallback. A rejected override contributes nothing, so the key resolves from the next layer: the stored value when one exists, else the manifest default. None of the five shipped format-typed keys declares a default, so the default is
null. Reads report that layer as the source, and the key is no longer locked. Both legs are pinned. The changeset states this for operators.Pending release note corrected
This PR deletes
.changeset/24882-settings-format-warning.md, PR #25001's changeset, and replaces it with.changeset/24882-settings-format-refusal.md.warnline is logged, and that refusal "waits for the next major version".@objectstack/service-settingsis at18.0.0, and itsCHANGELOG.mdhas no entry for it.This is the DELIBERATE CORRECTION class that
check-empty-changeset.mjsnames. Its foreign-changeset refusal is red by design here, andCheck Changesetwill show red. The remedy that gate prescribes is this paragraph: the note and what changed under it, put on the PR for confirmation. The note is not restored, and noskip-changesetis applied.Tests
The pin file is renamed:
settings-format-warning.test.tsbecomessettings-format-refusal.test.ts, with 41 cases.registerSettingsRoutesover a realSettingsService). Status starts at 0, so every status is asserted, not defaulted.get,getNamespace, theGETroute and thePUTlock.What the pins hold:
branding.logo_url,company.website,branding.accent_color,company.primary_contact_email,mail.from_email) and a syntheticphonekey are covered.400 SETTINGS_VALIDATIONwith exactly one field error:field, ADR-0114code,constraint.type, novalue, and it parses underFieldErrorSchema. Nothing is stored, and no log line is written.default,locked: false, noenventry in the cascade chain.409.visible-TOUCH pin.Readings on the final head
58d0be84f, each throughscripts/pm/os-verify-lock.sh(VERDICT command-exit 0):pnpm --filter @objectstack/service-settings run typecheck: exit 0. The program includes the new test (tsc --listFiles, 57 test files).pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2: 57 files, 970 tests passed.pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2overauth-settings-ordering.pin,auth-settings-seeded-boot.pinandauth-settings-refusal-isolation: 3 files, 13 tests passed. Their config aliasesservice-settingsto source.pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2overtest/settings-audit-global-rung-sibling,test/settings-config-change-auditandtest/settings-organization-isolation: 3 files, 14 tests passed. The rebuiltservice-settingsdist carries the refusal. No dogfood fixture saves a format-typed key.Ablations
Each leg ran with the implementation committed, on the file blob
ae561682b08a. The mutation went throughnode scripts/ablation-replace.mjsin WRAP mode, which proves the anchor fell 1 to 0 and the blob changed on disk. The restore after every leg is proven: blob == HEAD (ae561682b08a) and an emptygit diff HEAD. The tests import the service from source, so nodist/is involved.visible-TOUCH pin (expected 200 to be 400,expected 'env' to be 'default'). The 17 green are the well-formed, other-shape, empty, reset, fixture and env-precedence pins.shapebranch disabled)expected '… env override …' not to contain 'not a url'). The secret pin stays green because the encrypted-key redaction still applies, as expected.valueto not have property "value") and the secret save pin (not to contain 'billing-desk-at-corp-internal').expected 400 to be 200) and threemailpins that then trip unrelated required keys. This shows the stored-row pins can fail.A3's first attempt was a no-op that the tool refused: the replacement contained the anchor, so the anchor count did not fall. It was re-run with a non-overlapping replacement. The table records only the leg that landed.
Gates
Gate commands were derived from this worktree with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) on58d0be84f: 67 commands, the same set as the PM's reading at93e6c1a5f. Each ran locally with its exit code captured before any pipe.--ranreconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.node scripts/check-empty-changeset.mjs --base origin/mainexited 1. This is the designed red for the deliberate correction above.pnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET, no dist) on the battery. It was re-run with exit 0 after a full turbo build (112 entries across 69 packages load).check:dts-closureandcheck:sourcemap-no-sources-contentre-ran with exit 0 over the full build (76 and 72 packages).check-changeset-fixed.mjs,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity.CI owns the families outside that list.
Acceptance notes
content/docs/deployment/environment-variables.mdxsays the settings layer'sOS_BRANDING_LOGO_URL/OS_BRANDING_ACCENT_COLORoutrank a saved value and lock the field. That stays true for a well-formed value. The page does not say a malformed one is now ignored. Docs are outside this card's file surface. Owner: none.OS_LOGO_URL/OS_BRAND_COLORthatRuntimeConfigPluginreads are a different reader and are not judged by the settings door. This is unchanged here. Owner: none.origin/mainmoved one commit after the merge (aaee0840b,plugin-securityonly). It was not re-merged, so the measured head stays58d0be84f.Generated by Claude Code