Uh oh!
There was an error while loading. Please reload this page.
fix(form): refuse invented namespaced field widget ids, and cover the remaining secret spellings - #5448
Merged
Conversation
… remaining secret spellings
Three field-`type` spellings still put a secret on screen in clear text on the
form renderer's unregistered-widget branch. Measured on `main` at `f2e11ae6f`,
the real `form` renderer on the built-in path (no `registerAllFields()`), before
and after objectui#5322's fix:
type registry hit rendered type
ui:password true text
secret false text
field:secret false text
Maintainer ruling of 2026-08-20 — C + A, in one landing.
C (the primary, `@object-ui/core`): a form field's widget id that names a
namespace other than `field:` resolves NO field widget on the field path
(objectui#5254) and silently degrades to a plain text box. `validateFormSchema`
now reports it as an ERROR (`UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`), so
`assertValidSchema` throws and `validateSchema(...).valid` is false. Deliberately
NOT a warning: a warning is the same silent degrade under a new name. `field:`
ids stay legal whether or not the widget is registered — registration is a
runtime fact — and bare names stay legal because they are an open set.
A (defense in depth, `@object-ui/components`): bare `secret` and `ui:password`
join `NATIVE_INPUT_FIELD_TYPES` (native masked input) and `secret` joins
`SECRET_FIELD_TYPES`, so `field:secret` is refused exactly as `field:password`
is. Existing authors need zero migration, and a host that never validates still
does not leak.
`ui:password` IS registered — as an SDUI node renderer for a top-level
`{ type: 'email' }`-style node — so an author who checked whether it resolved
got a yes and still got a clear-text box on the field path. That is why the
class, not another table entry, is the primary fix.
The census the ruling made this conditional on: 4,397 authored files, 649
form-field `type`/`widget` literals, 98 structurally parsed form-field entries in
`examples/schema-catalog`. Seven colon-qualified ids on the form-field path, five
of them `field:*`; the remaining two occurrences are one source line naming
`ref:component`, a metadata-admin designer widget on a different shape this check
never reaches. Zero occurrences of any of the three measured spellings.
No producer emits any of the three — all are reachable only through a
hand-authored standalone form schema. Not a p0.
Fixes#5375
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE…ret-widget-spellings
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 20, 2026 21:17
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5375
Three field-
typespellings still put a secret on screen in clear text on the form renderer's unregistered-widget branch. Measured onmainatf2e11ae6f, the realformrenderer on the built-in path (noregisterAllFields()), before and after the earlier fix:For contrast,
passwordrenders a native masked input andfield:passwordis refused outright.Maintainer ruling of 2026-08-20, accepted verbatim (「其他接受你的建议。」): C + A, landed together.
The earlier fix closed this class only halfway — say so plainly
PR #5374 / issue #5322 is titled as if the "a secret shown in clear text" condition were shut. It is shut on the producer-reachable path only.
mapFieldTypeToFormTypemaps the ObjectQLsecrettype tofield:password, so every object-derived secret field is answered there. A hand-authored standaloneFormSchemadoes not go through that mapping — the author is the producer and no normalizer sits in between — and the three spellings above walked straight past the fix. #5322 remains closed and is not re-opened by this PR; this is the difference, recorded so the next reader is not misled.C (the primary) — an invented namespaced widget id is an authoring ERROR
packages/core/src/validation/schema-validator.tsA form field's widget id may name the
field:namespace or a bare name. Any other namespace resolves no field widget on the field path (that is #5254's ruling, andrenderFieldComponentimplements exactly it), so it fell through to the plain-textdefaultarm and degraded silently.validateFormSchemanow reportsUNRESOLVABLE_FIELD_WIDGET_NAMESPACEas an error, sovalidateSchema(...).validisfalseandassertValidSchemathrows.Why the class rather than another table entry:
ui:passwordIS registered — as an SDUI node renderer for a top-level{ type: 'email' }-style node — so an author who checks whether it resolves gets a yes, and still gets a clear-text box as a field. That is the shape where verifying does not protect you. A renderer-side table answers today's three spellings and leaves the next invented id the same silent degrade, which is literally how this card came to exist after #5322: that fix keyed on thefield:-stripped type andui:passwordwalked past it. AI-authored metadata invents plausible-looking widget ids constantly; this makes inventing one fail loudly.Deliberately not a warning — the ruling rules that out by name, because a warning is the same silent degrade under a new spelling.
What stays legal, and why:
field:id, registered or not. Registration is a runtime fact (registerAllFields(), a lazily loaded plugin) an authoring-time validator cannot see, and Afield:-prefixedpassword/emailfield renderstype="text"when its widget is not registered — a secret shown in clear text #5322 already answers an unregisteredfield:secret with a visible refusal.field:namewidget, third-party ones included, is reachable by its short name.What is decidable statically is the namespace, and it is a closed set of one.
The check reads
widgetthentype, mirroring the renderer's ownresolvedTypeprecedence, so validator and renderer cannot disagree about which component will actually render.The census — the ruling made arming this conditional on one
Scanned this repo's authored surface two ways: a textual pass over every authored file (bracket-matching each
fields: [ ... ]region and collecting thetype/widgetliterals inside it) and a structural pass that parses every JSON/YAML and walks form-field-shaped objects.type/widgetliterals (textual)examples/schema-catalog)field:*(stay legal)field:namespaceui:password/ baresecret/field:secreton the field pathThe five
field:*arefield:text,field:selectand two template literals (field:${type}). The one non-field:id isref:component, atpackages/app-shell/src/views/metadata-admin/RefComponentWidget.test.tsx:174(counted twice because it sits inside two nestedfields:arrays). It is out of the gated shape: a metadata-admin designerSchemaFormspec form-VIEW field ({ field, widget }undersections[].fields[], node typesimple), resolved by app-shell's ownWIDGETStable, never byrenderFieldComponent. This check is gated ontype === 'form'with runtimefields[].name, so it never reaches it — pinned as a test.Counter-probe, and it caught a real methodology bug. The first run reported zero hits under
examples/. Probing for form fields I knew were present (examples/schema-catalog/src/schemas/components-form-form/*.json) showed the scan's key regex did not accept JSON-quoted keys ("type":), only unquoted ones. After the fixexamples/schema-catalogreports 133 textual and 98 structural hits, including barepasswordthree times. So the zero above is a real zero, not a broken probe.Small count, all hits clearly out of scope ⇒ the refusal is armed, per step 2 of the ruling's gate.
A (defense in depth, same landing) — the known secret tables
packages/components/src/renderers/form/form.tsxNATIVE_INPUT_FIELD_TYPESgainssecretandui:password, so both render the native masked input.SECRET_FIELD_TYPESgainssecret, sofield:secretis refused outright exactly asfield:passwordis — no input, no value in the DOM, and arole="alert"box naming the missing widget.Zero migration: an existing author who wrote either spelling keeps a working field, and a host that never validates still does not leak.
ui:passwordis keyed raw, not as a declared type, and deliberately so —normalizeFieldTypestrips onlyfield:, and givingui:a stripping rule of its own would re-open the cross-namespace resolution #5254 removed and would silently moveui:email,ui:dateand every id this card never measured. A test pinsui:emailstill renderingtype="text"so that widening cannot happen unnoticed.Fences honoured
type: 'email'经 bare-name 回退落进 SDUI 节点渲染器,field(对象)与max_length一起泄漏到 DOM #5254 routed these spellings to thedefaultarm; that ruling stands.ui:passwordstill resolves no widget — it only picks a native input type.packages/reactwas not touched — the validate path for C lives inpackages/core, which the dispatch cleared.packages/reactconsumes the changed contract unmodified. PR fix(components,layout): emit explicit extensions in the published typings so nodenext consumers can follow them #5441 landed mid-task;mainwas merged in (fe76ece3a) and every gate re-run on the merged head.Verification, at
420811271Both halves reverse-verified separately, direction predicted before running. No build sits between the edits and the tests: the root
vitest.config.mtsaliasblock maps every@object-ui/*specifier to that package'ssrc/, and both files reach their subject by relative path, so nodist/is consulted on any leg. Each ablation was taken from the committed fix and restored bygit checkout branch -- path, proven byte-identical with an emptygit diff --stat HEAD.form.tsxreverted)ui:passwordandsecretrendertype="text",field:secretrenders an input instead of the refusalexpected true to be falseonresult.validThe A-leg dump is the defect verbatim:
type="text" value="hunter2"for afield:secretfield.Green readings on the merged head:
No test was skipped, disabled or quarantined.
A changeset is included (
minoron both packages; the@object-ui/corehalf is a behaviour change — a schema that previously validated with an invented namespaced id is now invalid).Generated by Claude Code