Uh oh!
There was an error while loading. Please reload this page.
fix(components): a field:-prefixed secret field fails closed instead of rendering clear text (#5322) - #5374
Merged
os-support-ai merged 1 commit intoAug 20, 2026
Conversation
…d of rendering clear text (#5322) On the built-in path the `field:`-prefixed widget id resolves nothing and takes the form renderer's `default` input branch, whose native-input table was keyed on the raw `type`. The prefixed spelling missed it and rendered `type="text"` — `mapFieldTypeToFormType` emits that id for every object-derived form, so an object-derived password field put the secret on screen in clear text. - `field:password` now refuses: no input, no value in the DOM, an inline `role="alert"` naming the missing widget and a `console.error` carrying the fix. - `field:email` renders the native email input (the table is keyed on the declared type, prefix stripped). - The bare `password` / `email` spellings are untouched. Pre-existing, not a regression from #5254. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 20, 2026 02:53
Uh oh!
There was an error while loading. Please reload this page.
os-support-ai
deleted the
claude/issue-5322-prefixed-field-native-input
branch
August 20, 2026 02:53
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#5322
The defect, re-measured at this branch point
mainatf2e11ae6f. On the built-in path (@object-ui/fieldsnot registered), rendered through the realformrenderer:A
field:-prefixed id resolves nothing, takesrenderFieldComponent'sdefaultarm, and missesNATIVE_INPUT_FIELD_TYPESbecause that table is keyed on the rawtype.mapFieldTypeToFormTypeemits the prefixed id for every object-derived form, so this is the normal path: an object-derivedpasswordfield put the secret on screen in clear text, and an object-derivedemailfield lost its native keyboard and validation.Pre-existing. The table is keyed on the raw type, so the prefixed spelling never matched it — the same reading was taken on
origin/mainbefore #5254 landed. #5254 / PR5326 neither introduced nor fixed this.The shape chosen, and why
Triage ruled: "the unregistered-widget default must respect the declared input type, or refuse to render the value rather than degrade to clear text", with "for
passwordspecifically, prefer masking/refusal over best-effort rendering". Both limbs are used — on the spelling where each is correct, because the two spellings do not mean the same thing:password/emailis a declared input type on the built-in path. It claims no registered widget; this branch is its intended home and its native input is the correct rendering, not a degrade. Untouched.field:-prefixed id is a registry key. Reaching the default arm with one proves an unmet contract: the app declares a widget that is not registered.So:
field:passwordtype="text"— secret in clear textfield:emailtype="text"type="email"passwordtype="password"type="password"(unchanged)emailtype="email"type="email"(unchanged)Why refusal and not masking for
field:password. Masking alone closes the leak, and it is exactly what the bare spelling does. It is not enough here, because here we additionally know the app shipped without the widget it declares: a masked box would invite the user to type a secret into a form whose password widget — strength meter, confirm pair, reveal toggle, submit handling — is absent, in a control that looks like it worked. The refusal is an inlinerole="alert"naming the missing widget id, plus aconsole.errorwhose text doubles as the fix instruction (registerAllFields()), mirroringRetiredFieldTombstoneinpackages/fieldsand this file's own spec-vocabulary boundary (#3090). Nothing is thrown — one unrenderable field must not take down the rest of a record form.Why
field:emailgets the other limb. No leak, and the ruling's first limb applies directly: respect the declared type. Turning every unregisteredfield:*id into a refusal would changefield:currency,field:qrcodeand every other type this card neither moved nor measured, and would break every app that renders forms without registering fields.Deliberately narrow: only
passwordrefuses, only under thefield:namespace, and a registeredfield:passwordwidget still wins. Aui:-qualified id renders exactly as it does today.No new authorable key
The accept set is unchanged — no metadata key is declared, read, or widened.
NATIVE_INPUT_FIELD_TYPESis now keyed on the declared type (normalizeFieldType, already in this file) instead of the raw one; the refusal keys on thefield:prefix plus a two-lineSECRET_FIELD_TYPESset. This narrows what renders; it accepts nothing new.Verification
Pinned on the rendered output, not the resolver — the whole defect is that a resolver returning nothing still produced a plausible-looking control.
New file:
packages/components/src/renderers/form/__tests__/form-prefixed-field-native-input.test.tsx, 16 tests, covering both spellings and both types plus the no-collateral cases (a registeredfield:passwordwidget wins;field:currencystill renders its text box; builtininputuntouched; the declared ceiling still caps).Verified at
6a668d777:Run from the repo root with no
--before the paths, perscripts/vitest-invocation-guard.mjs.Reverse verification
form.tsxreverted toorigin/mainwith the new test file kept, then re-run:type="text"appears in the failure text on thefield:passwordlegs, and the seeded-secret leg prints the pre-fix DOM carryingvalue="hunter2"in clear text. The 10 that stay green are the bare-spelling pins and the no-collateral cases — i.e. the file would also catch a fix that traded one spelling for the other.form.tsxwas restored from the commit afterwards (git checkout HEAD -- …), verified byte-identical by an emptygit diff HEAD.Build artifacts
None on any leg. The root
vitest.config.mtsaliases every@object-ui/*specifier to that package'ssrc/, and the renderer under test is imported through a relative path, so nodist/sits between the edit and the thing under test. The dependency closure (pnpm --filter '@object-ui/components^...' build) was built only becausetype-checkresolves@object-ui/coreand@object-ui/typesthrough their published.d.ts— without it that leg fails withTS2307 Cannot find module, which reads like a source error and is not one.check:doc-snippetswas not run locally: it refuses to start until every package it resolves against has a builtdist/, unrelated to this change. CI builds first and runs it there.Notes
RetiredFieldTombstonemakes.+1eslint warning onform.tsx(react-refresh/only-export-components, for the new component), the same warning the two neighbouring built-in-branch components already carry.--max-warningsis deliberately unset inlint.yml.Generated by Claude Code