IS-11327 LWA: WebAuthn ceremony errors as HAAPI step AppErrors#194
Conversation
Catch every WebAuthn ceremony failure in the runners and surface it as a
synthesised HaapiUnexpectedProblemStep via the existing error.app pipeline
(rather than escalating to the React error boundary as a programming bug).
Two-bucket discriminator (cancelOrTimeout / failed) matching Velocity
parity. Copy comes from step.metadata.messages.error.clientOperation.webauthn
with empty-string fallback while the BE keys land.
* Runner-level synthesis: runWebAuthn{Registration,Authentication} catch the
full error catalog (parse DOMException, create/get DOMException, TypeError,
null credential, unsupported API) and throw a synthesised HaapiStepperError
built via formatErrorStepData(HaapiUnexpectedProblemStep).
* Dispatcher wrapping: performClientOperation wraps each WebAuthn runner in a
.then/.catch chain returning ClientOperationResult discriminated union. The
catch discriminates HaapiStepperError vs raw rejections via a type guard;
raw errors propagate to the React boundary as programming bugs.
* Type hierarchy: HaapiWebAuthnRegistrationClientOperationAction is now a
proper discriminated union (passkeys | any-device) so consumers narrow via
positive + negative type guards without casts. HaapiWebAuthnAnyDeviceArgs
is also a discriminated union (platform-required | crossPlatform-required)
expressing the "at least one of two" HAAPI spec invariant.
* Metadata surface: HaapiMetadata.messages.error.clientOperation.webauthn
carries the per-ceremony copy (cancelOrTimeout shared, registration,
authentication).
* Auto-start parity: manageWebAuthnAutoStart stays fire-and-forget via
nextStep(); ceremony failures surface the same way as manual click
(matching Velocity's .catch(handleError) pattern).
* Tests: 23 runner-level tests in webauthn.spec.ts cover the full catalog
per ceremony; HaapiStepper.spec.tsx wiring tests consolidated under one
WebAuthn suite (Auto-Start gating + Registration/Authentication
success+error routing), dropping the duplicate auto-start error-surfacing
cases since manual click + auto-start share the dispatcher plumbing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…apiStepperError * Rename `HaapiMetadata.messages` → `HaapiMetadata.viewData` (and the dependent type aliases) so the metadata branch reads as view-customisation data emitted by the server rather than HAAPI step messages. Path becomes `step.metadata.viewData.error.clientOperation.webauthn.<key>`. * Extract `getHaapiStepperError` from `webauthn.ts` to `client-operations.ts` as a shared helper that takes a resolved message string and synthesises a `HaapiStepperError` from a `HaapiUnexpectedProblemStep`. WebAuthn-specific message resolution stays in `webauthn.ts` (`getWebAuthnErrorMessage`); other client operations (BankID, EBF) can reuse the helper when their per-runtime error handling lands. * Test fixtures + path-access assertions in `webauthn.spec.ts` updated to use the new `viewData` key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClientOperationResult is no longer WebAuthn-specific — the EBF and BankID runners will need it next. Extract it into a new sibling typings.ts file alongside the operation runners; keep the WebAuthn-specific enums where they are. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flip the runtime semantics from `!!field.required` to `field.required ?? true`
on the four visible field UIs (text, password, select, checkbox). Matches the
JSDoc on HaapiBaseFormField.required ("Defaults to true.") and reflects the
production reality that most LWA form fields are required.
Update HaapiStepperFormUI.spec.tsx to keep `createLoginFormAction()` mirroring
production (no `required: false` overrides) and instead fill the fields each
test actually needs to clear via per-field helpers (fillUsername, fillPassword,
selectCountry, checkRememberMe).
- Render theme.logo.path as <img className="haapi-stepper-logo">, positioned before or inside <Well> based on theme.logo.isInsideWell. - Add .haapi-stepper-logo CSS reusing --login-logo-* theme tokens (200px max-width, 40px max-height, object-fit: contain) and mirror legacy spacing (1.5rem mobile, 5rem >=576px outside the well, 0 inside). - Decorative alt="" + role="presentation".
luisgoncalves
left a comment
There was a problem hiding this comment.
I did a few tests and seems OK. I also skimmed on the code changes and they are bigger than I imagined. I see there are other reviews ongoing, so I'm adding my comments but leaving the final review for others that started before I did.
…8-required-attr-form-fields
…-page-symbols-data IS-11318 LWA: rework page symbols data in bootstrap configuration
…form-fields IS-11368 LWA: forward HaapiBaseFormField.required to rendered field inputs
…eb-app/IS-11345-display-logos IS-11345 Display configured logo in the Login Web App
Narrows the WebAuthn-runner classifier to honour the typings.ts contract: only DOMException routes to error.app (NotAllowed/Abort -> CANCEL_OR_TIMEOUT, any other DOMException -> FAILED). Anything else (TypeError, helper throw, malformed-payload access errors, etc.) propagates so the React error boundary catches it instead of being buried under 'Registration/Authentication failed' copy. Keeps Velocity-parity for users and covers future WebAuthn-spec DOMException names without an allowlist. Spec updated: the registration/authentication asserts that TypeError / 'arbitrary non-DOMException' map to the FAILED copy are flipped to .rejects.toBe(error), and the authentication side gains symmetric coverage. Addresses PR #194 review feedback from @vahag-curity and @luisgoncalves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches mockRunWebAuthnRegistration / mockRunWebAuthnAuthentication from bare vi.fn() (Mock<any, any>) to vi.fn<typeof runWebAuthnX>(). With the runner contract Promise<ClientOperationResult> in scope, TypeScript now rejects mock fixtures that resolve outside the shape — including mockResolvedValue(undefined) which previously compiled silently and TypeError'd at runtime. The Auto-Start beforeEach baseline is updated to resolve to a sentinel clientOperationError: failedWebAuthnCeremonyError() so an unintended runner invocation surfaces loudly via error.app instead of progressing the stepper silently. Addresses PR #194 review feedback from @vahag-curity: the dispatcher's destructure has no production hazard — strict mode already prevents runners from falling off the end of Promise<ClientOperationResult>. The hazard was test-fixture authoring; this commit closes it at that layer rather than adding a runtime guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Follow-up to IS-11252: WebAuthn ceremony failures (cancel/timeout, parse errors, unsupported API, null credential) are now caught inside the WebAuthn runners and surfaced as client-synthesised AppError-class HaapiStepperErrors via useHaapiStepper().error.app, rather than throwing to the React error boundary. Runners now share a ClientOperationResult discriminated union; performClientOperation is restructured to propagate errors through the stepper instead of try/catch with null returns. Step metadata gains a viewData.error.clientOperation.webauthn.{cancelOrTimeout|registration|authentication} channel that supplies the user-facing copy.
Changes:
- Introduce a
ClientOperationResultshape and refactorperformClientOperation+ WebAuthn runners to return{clientOperationData}/{clientOperationError}rather than throw. - Classify caught WebAuthn errors via a two-bucket discriminator (
cancelOrTimeout/failed) and resolve copy fromcurrentStep.metadata.viewData.error.clientOperation.webauthn. - Restructure WebAuthn tests around the new result type, add error-bucket coverage, and extend
HaapiMetadata/WebAuthn action types accordingly.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
feature/actions/client-operation/operations/webauthn/webauthn.ts |
Wraps ceremonies in try/catch, classifies errors, returns ClientOperationResult; threads currentStep for message lookup. |
feature/actions/client-operation/operations/webauthn/webauthn.spec.ts |
Replaces "throws" tests with "resolves with clientOperationError" tests covering DOM exceptions, null credentials, missing API, and metadata fallback. |
feature/actions/client-operation/operations/webauthn/typings.ts |
New WEBAUTHN_ERROR_TYPE / WEBAUTHN_OPERATION enums for bucket selection. |
feature/actions/client-operation/operations/typings.ts |
New ClientOperationResult discriminated-union shared across runners. |
feature/actions/client-operation/operations/client-operations.ts |
performClientOperation returns ClientOperationResult; adds getHaapiStepperError synthesiser; drops AbortError swallowing. |
feature/stepper/HaapiStepper.tsx |
Adapts to the new client-operation return shape and forwards currentStep + clientOperationError into nextStepError. |
feature/stepper/HaapiStepper.spec.tsx |
Reorganises WebAuthn tests under Auto-Start vs Registration/Authentication describes and adds error / retry coverage via error.app. |
data-access/types/haapi-step.types.ts |
Adds HaapiMetadataViewData chain typing the per-step WebAuthn error copy. |
data-access/types/haapi-action.types.ts |
Reworks WebAuthn registration action union and tightens any-device args to enforce at least one creation-options field. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…-ui-with-error-notifier IS-5161 Wrap HaapiStepperStepUI with HaapiStepperErrorNotifier
…ader-fix-config IS-11318 LWA: fix propagation of bootstrap configuration in the dev setup
Catch every WebAuthn ceremony failure in the runners and surface it as a
synthesised HaapiUnexpectedProblemStep via the existing error.app pipeline
(rather than escalating to the React error boundary as a programming bug).
Two-bucket discriminator (cancelOrTimeout / failed) matching Velocity
parity. Copy comes from step.metadata.messages.error.clientOperation.webauthn
with empty-string fallback while the BE keys land.
* Runner-level synthesis: runWebAuthn{Registration,Authentication} catch the
full error catalog (parse DOMException, create/get DOMException, TypeError,
null credential, unsupported API) and throw a synthesised HaapiStepperError
built via formatErrorStepData(HaapiUnexpectedProblemStep).
* Dispatcher wrapping: performClientOperation wraps each WebAuthn runner in a
.then/.catch chain returning ClientOperationResult discriminated union. The
catch discriminates HaapiStepperError vs raw rejections via a type guard;
raw errors propagate to the React boundary as programming bugs.
* Type hierarchy: HaapiWebAuthnRegistrationClientOperationAction is now a
proper discriminated union (passkeys | any-device) so consumers narrow via
positive + negative type guards without casts. HaapiWebAuthnAnyDeviceArgs
is also a discriminated union (platform-required | crossPlatform-required)
expressing the "at least one of two" HAAPI spec invariant.
* Metadata surface: HaapiMetadata.messages.error.clientOperation.webauthn
carries the per-ceremony copy (cancelOrTimeout shared, registration,
authentication).
* Auto-start parity: manageWebAuthnAutoStart stays fire-and-forget via
nextStep(); ceremony failures surface the same way as manual click
(matching Velocity's .catch(handleError) pattern).
* Tests: 23 runner-level tests in webauthn.spec.ts cover the full catalog
per ceremony; HaapiStepper.spec.tsx wiring tests consolidated under one
WebAuthn suite (Auto-Start gating + Registration/Authentication
success+error routing), dropping the duplicate auto-start error-surfacing
cases since manual click + auto-start share the dispatcher plumbing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…apiStepperError * Rename `HaapiMetadata.messages` → `HaapiMetadata.viewData` (and the dependent type aliases) so the metadata branch reads as view-customisation data emitted by the server rather than HAAPI step messages. Path becomes `step.metadata.viewData.error.clientOperation.webauthn.<key>`. * Extract `getHaapiStepperError` from `webauthn.ts` to `client-operations.ts` as a shared helper that takes a resolved message string and synthesises a `HaapiStepperError` from a `HaapiUnexpectedProblemStep`. WebAuthn-specific message resolution stays in `webauthn.ts` (`getWebAuthnErrorMessage`); other client operations (BankID, EBF) can reuse the helper when their per-runtime error handling lands. * Test fixtures + path-access assertions in `webauthn.spec.ts` updated to use the new `viewData` key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClientOperationResult is no longer WebAuthn-specific — the EBF and BankID runners will need it next. Extract it into a new sibling typings.ts file alongside the operation runners; keep the WebAuthn-specific enums where they are. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Narrows the WebAuthn-runner classifier to honour the typings.ts contract: only DOMException routes to error.app (NotAllowed/Abort -> CANCEL_OR_TIMEOUT, any other DOMException -> FAILED). Anything else (TypeError, helper throw, malformed-payload access errors, etc.) propagates so the React error boundary catches it instead of being buried under 'Registration/Authentication failed' copy. Keeps Velocity-parity for users and covers future WebAuthn-spec DOMException names without an allowlist. Spec updated: the registration/authentication asserts that TypeError / 'arbitrary non-DOMException' map to the FAILED copy are flipped to .rejects.toBe(error), and the authentication side gains symmetric coverage. Addresses PR #194 review feedback from @vahag-curity and @luisgoncalves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches mockRunWebAuthnRegistration / mockRunWebAuthnAuthentication from bare vi.fn() (Mock<any, any>) to vi.fn<typeof runWebAuthnX>(). With the runner contract Promise<ClientOperationResult> in scope, TypeScript now rejects mock fixtures that resolve outside the shape — including mockResolvedValue(undefined) which previously compiled silently and TypeError'd at runtime. The Auto-Start beforeEach baseline is updated to resolve to a sentinel clientOperationError: failedWebAuthnCeremonyError() so an unintended runner invocation surfaces loudly via error.app instead of progressing the stepper silently. Addresses PR #194 review feedback from @vahag-curity: the dispatcher's destructure has no production hazard — strict mode already prevents runners from falling off the end of Promise<ClientOperationResult>. The hazard was test-fixture authoring; this commit closes it at that layer rather than adding a runtime guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…:curityio/ui-kit into feature/IS-11327/webauthn-error-handling
Decouples the WebAuthn runner from the client-operation dispatcher. Previously the runner imported `getHaapiStepperError` from `client-operations.ts`, which itself imports the runners — an awkward upward dependency. Co-locating the helper with `ClientOperationResult` (typings.ts) by way of a sibling `helpers.ts` follows the same shape used for the shared result type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Jira: https://curity.atlassian.net/browse/IS-11327
Summary
Follow-up to IS-11252. Catches WebAuthn ceremony exceptions (cancel / timeout / parse error / unsupported API / null credential), classifies them via a two-bucket discriminator (
cancelOrTimeout/failed), and surfaces them as client-synthesisedAppError-class problems throughuseHaapiStepper().error.app. Copy comes fromstep.metadata.messages.error.clientOperation.webauthn.{cancelOrTimeout | registration | authentication}. Since the BE is not providing the error copies yet, it currently shows the default error message ('An error occurred').Test plan
Setup:
HaapiStepperStepUIwithHaapiStepperErrorNotifierin `App.tsxwebauthnauthenticatorTest cases:
error.appsubscription.