Repository navigation
fix(web): defer issuer validation to onBlur, block silent-drop on close, require a real host URL (closes #1544) - #1545
Conversation
) The Enterprise-Managed Authorization Issuer field validated its URL inline on every render, so the 'Must be an http(s) URL' error nagged while the user was still mid-typing (e.g. before 'https:/…' is complete). Gate the issuer error display on an issuerTouched flag set onBlur; once touched it tracks the value live, so the error clears as soon as a valid URL is entered. The persist gate (canPersistClientSettingsDraft → validateClientSettings) is unchanged and still validates independently, so an invalid issuer is never written to the backend regardless of touched state. Closes #1544 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the on-blur deferral so an invalid issuer can't be silently dropped on modal close (the auto-save flush would otherwise discard it with no feedback). ClientSettingsModal now intercepts its close (X / Esc / overlay): if the issuer is invalid it reveals the field error (via the form's revealIssuerError prop, overriding on-blur gating) and keeps the modal open instead of closing. The user can fix the URL or clear the field — an empty issuer is valid to leave. The block lives in the modal (unit-tested) rather than App, so App.tsx stays a thin flush+close that only runs when the close is allowed; the persist gate is unchanged. Reveal state resets via the modal's open/close remount key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(s) scheme (#1544) The issuer validation used URL.canParse + an http(s) protocol check, which the WHATWG parser accepts for bare/degenerate values that aren't actually usable URLs — e.g. https://foo, https://example, https://. , https://.. all passed. Tighten the shared rule (used by both the Zod config schema and the form's inline validation): after parsing and the http(s) scheme check, require the host to actually look like a host — localhost, an IPv6 literal, or a dotted domain/IPv4 with at least two non-empty labels. Single-label, bare-dot, and empty-label hosts are now rejected. Add core tests for the accepted (dotted/IPv4/IPv6/localhost) and newly-rejected cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @cliffhall's task in 1m 57s —— View job Review: PR #1545 — defer issuer validation, block silent-drop, require real host
Nice, well-scoped change. The three concerns from #1544 are each handled in the right layer (form gating, modal close-block, core URL check), the comments explain the why clearly, and the test coverage is thorough across all three. The modal remount 1. (Worth deciding)
|
|
Thanks for the thorough review. Decisions on the four items: 1. Single-label hosts — keeping strict (no change). The reviewer's Docker/K8s reflex is for server-to-server resolution, but the Inspector's OAuth is browser-driven: the user's browser is redirected to the IdP's authorization endpoint, and a browser on the host can't resolve a container-internal name like 2. Trailing-dot FQDN — intentionally rejected. Consistent with (1)'s strictness; 3. Incomplete EMA (valid issuer, empty clientId) silently dropped — filed as #1546. Good catch; it's the same silent-drop class for a different field and out of #1544's issuer-only scope, so I opened a follow-up to extend the close-block/reveal treatment to the other required EMA fields. 4. Thanks also for verifying the remount-key reset and the IPv6 bracket handling. |
|
@claude review |
|
Claude finished @cliffhall's task in 1m 9s —— View job Re-review: PR #1545 — defer issuer validation, block silent-drop, require real host
Re-reviewed against Verifying the decisions land in the code
One small observation (non-blocking)
Confirmed clean
I did not run the suite locally for this pass; relying on the reported |
|
Thanks for the re-review. On the all-numeric/out-of-range observation ( @claude review |
|
Claude finished @cliffhall's task in 46s —— View job Re-review: PR #1545 — defer issuer validation, block silent-drop, require real host
Agreed on the structural-vs-semantic call — and there's a concrete reason it's the right boundary, not just an acceptable one: Nothing new on this pass — still good to merge. Quick confirmation the code matches every decision in the thread:
The persist gate remains the independent backstop, so an invalid issuer is never written regardless of touched/blur/reveal state. Coverage spans all three layers. I relied on the PR-reported |
…MD check to use same mechanism as EMA Issuer.
…ssuer (closes #1546) (#1547) Follow-up to #1545. The close-block only fired for an invalid issuer, so an EMA config enabled with a valid issuer but a blank client ID was still silently dropped on close by the persist gate — the same no-feedback bug, different field. - validateClientSettings gains a requireComplete mode that also flags the required IdP fields (issuer, clientId) when blank, with distinct per-field messages. Inline (as-you-type) validation is unchanged — required-but-blank errors only surface on reveal, so the form never nags about not-yet-filled fields. - canPersistClientSettingsDraft is redefined in terms of that mode, so the persist gate and the close-time field errors share one source of truth. - ClientSettingsModal.handleClose now blocks + reveals on ANY require-complete error (invalid issuer OR blank required field) and keeps the modal open. The form prop revealIssuerError becomes revealErrors and surfaces the client-ID error too. Escape hatch: disable enterprise IdP (an empty issuer no longer closes, since that's itself incomplete). Tests cover the blank-clientId close path, fix-each-field, and disable-EMA escape, plus requireComplete validation. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #1544. Follow-up to #1533 (inline issuer validation).
Problem
The Enterprise-Managed Authorization Issuer field validated on every render, so the "Must be an http(s) URL" error nagged while the user was still mid-typing (after
h,ht,http:/…). And because Client Settings auto-saves (debounced + flush on close) with no Save button, a user could type an invalid issuer and close the modal without ever blurring — the persist gate silently dropped it with no feedback. Separately, the URL check accepted bare/degenerate values that merely parse ashttps://….Changes
1. Defer the issuer error to onBlur (
ClientSettingsForm.tsx)The issuer error is gated on an
issuerTouchedflag setonBlur; once touched it tracks the value live (clears as soon as it's valid). No more nagging mid-typing.2. Don't silently drop an invalid issuer on close (
ClientSettingsModal.tsx)The modal intercepts its close (X / Esc / overlay): if the issuer is invalid it reveals the error (overriding the on-blur gating, via the form's new
revealIssuerErrorprop) and keeps the modal open instead of closing. The user can fix the URL or clear the field — an empty issuer is valid to leave. The block lives in the (unit-tested) modal soApp.tsxstays a thin flush+close that only runs when the close is allowed.3. Require an actually-valid URL, not just an http(s) scheme (
core/client/config-parse.ts)isAbsoluteHttpUrl(shared by the Zod config schema and the form) previously accepted anythingURL.canParse+ http(s) allowed — includinghttps://foo,https://example,https://.,https://..,https://idp..example.com,https:///path. It now also requires the host to look like a host:localhost, an IPv6 literal, or a dotted domain/IPv4 with ≥2 non-empty labels.The persist gate (
canPersistClientSettingsDraft→validateClientSettings) is unchanged and still validates independently, so an invalid issuer is never written to the backend regardless of touched/blur state.Behavior summary
Screencapture
url-validate-on-blur.mov
Tests / verification
ClientSettingsFormtests: deferral on blur, live-clear,revealIssuerErrorprop.ClientSettingsModaltests: block-close-on-invalid, fix-then-close, clear-then-close.core/clienttests: accepted (dotted / IPv4 / IPv6 / localhost / unicode) and newly-rejected (bare, single-label, no-TLD, dots, empty-label) hosts.format:check,lint,tsc -ball clean.🤖 Generated with Claude Code