-
Notifications
You must be signed in to change notification settings - Fork 2
feat: prefer canonical awaiting-input contract #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bc9b0a0
4aa184f
30a2d0b
a59c8d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@onkernel/managed-auth-react": patch | ||
| --- | ||
|
|
||
| Prefer the canonical managed-auth awaiting-input contract (`fields` and `choices`) when present, while continuing to fall back to legacy `discovered_fields`, `pending_sso_buttons`, `mfa_options`, and `sign_in_options` during the deprecation window. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,9 @@ import { | |
| retrieveManagedAuth, | ||
| streamManagedAuthEvents, | ||
| submitFieldValues, | ||
| submitCanonicalFieldValues, | ||
| submitMFASelection, | ||
| submitSelectedChoice, | ||
| submitSignInOption, | ||
| submitSSOButton, | ||
| type ApiClientOptions, | ||
|
|
@@ -14,8 +16,13 @@ import { | |
| import type { | ||
| AuthErrorPayload, | ||
| AuthSuccessPayload, | ||
| DiscoveredField, | ||
| ManagedAuthChoice, | ||
| ManagedAuthField, | ||
| ManagedAuthResponse, | ||
| MFAType, | ||
| MFAOption, | ||
| SignInOption, | ||
| SSOButton, | ||
| UIState, | ||
| } from "../lib/types"; | ||
|
|
@@ -57,10 +64,13 @@ function mergeStateEvent( | |
| flow_status: ev.flow_status, | ||
| flow_step: ev.flow_step, | ||
| flow_type: ev.flow_type ?? base.flow_type ?? null, | ||
| discovered_fields: ev.discovered_fields ?? null, | ||
| pending_sso_buttons: ev.pending_sso_buttons ?? null, | ||
| mfa_options: ev.mfa_options ?? null, | ||
| sign_in_options: ev.sign_in_options ?? null, | ||
| fields: ev.fields ?? base.fields ?? null, | ||
| choices: ev.choices ?? base.choices ?? null, | ||
| discovered_fields: ev.discovered_fields ?? base.discovered_fields ?? null, | ||
| pending_sso_buttons: | ||
| ev.pending_sso_buttons ?? base.pending_sso_buttons ?? null, | ||
| mfa_options: ev.mfa_options ?? base.mfa_options ?? null, | ||
| sign_in_options: ev.sign_in_options ?? base.sign_in_options ?? null, | ||
|
Comment on lines
+67
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| external_action_message: ev.external_action_message ?? null, | ||
| website_error: ev.website_error ?? null, | ||
| error_message: ev.error_message ?? null, | ||
|
|
@@ -71,6 +81,128 @@ function mergeStateEvent( | |
| }; | ||
| } | ||
|
|
||
| function fieldTypeToDiscoveredType( | ||
| field: ManagedAuthField, | ||
| ): DiscoveredField["type"] { | ||
| switch (field.type) { | ||
| case "identifier": | ||
| return "email"; | ||
| case "totp_code": | ||
| return "totp"; | ||
| case "totp_secret": | ||
| return "text"; | ||
| default: | ||
| return field.type; | ||
| } | ||
| } | ||
|
|
||
| function fieldsFromCanonical( | ||
| fields?: ManagedAuthField[] | null, | ||
| ): DiscoveredField[] | null { | ||
| if (!fields?.length) return null; | ||
| return fields.map((field) => ({ | ||
| id: field.id, | ||
| ref: field.ref, | ||
| name: field.id, | ||
| type: fieldTypeToDiscoveredType(field), | ||
| label: field.label || field.ref, | ||
| required: field.required ?? true, | ||
| })); | ||
| } | ||
|
|
||
| function ssoButtonsFromCanonical( | ||
| choices?: ManagedAuthChoice[] | null, | ||
| ): SSOButton[] | null { | ||
| if (!choices?.length) return null; | ||
| const buttons = choices | ||
| .filter((choice) => choice.type === "sso_provider") | ||
| .map((choice) => ({ | ||
| id: choice.id, | ||
| provider: choice.id, | ||
| selector: choice.observed_selector || choice.id, | ||
| label: choice.label, | ||
| })); | ||
| return buttons.length ? buttons : null; | ||
| } | ||
|
|
||
| function normalizeMFAChoiceId(id: string): MFAType { | ||
| switch (id.trim().toLowerCase()) { | ||
| case "sms_code": | ||
| case "sms": | ||
| return "sms"; | ||
| case "email_code": | ||
| case "email": | ||
| return "email"; | ||
| case "totp_code": | ||
| case "totp": | ||
| case "authenticator": | ||
| case "authenticator_app": | ||
| return "totp"; | ||
| case "phone_call": | ||
| case "call": | ||
| return "call"; | ||
| case "push": | ||
| return "push"; | ||
| case "password": | ||
| return "password"; | ||
| case "passkey": | ||
| return "passkey"; | ||
| case "switch": | ||
| return "switch"; | ||
| default: | ||
| return "other"; | ||
| } | ||
| } | ||
|
|
||
| function mfaOptionsFromCanonical( | ||
| choices?: ManagedAuthChoice[] | null, | ||
| ): MFAOption[] | null { | ||
| if (!choices?.length) return null; | ||
| const options = choices | ||
| .filter((choice) => choice.type === "mfa_method") | ||
| .map((choice) => ({ | ||
| type: normalizeMFAChoiceId(choice.id), | ||
| label: choice.label, | ||
| description: choice.description ?? undefined, | ||
| })); | ||
| return options.length ? options : null; | ||
| } | ||
|
|
||
| function signInOptionsFromCanonical( | ||
| choices?: ManagedAuthChoice[] | null, | ||
| ): SignInOption[] | null { | ||
| if (!choices?.length) return null; | ||
| const options = choices | ||
| .filter( | ||
| (choice) => | ||
| choice.type !== "sso_provider" && choice.type !== "mfa_method", | ||
| ) | ||
| .map((choice) => ({ | ||
| id: choice.id, | ||
| label: choice.label, | ||
| description: | ||
| choice.description ?? choice.context ?? choice.display_text ?? null, | ||
| })); | ||
| return options.length ? options : null; | ||
| } | ||
|
|
||
| function normalizeManagedAuthState( | ||
| state: ManagedAuthResponse, | ||
| ): ManagedAuthResponse { | ||
| return { | ||
| ...state, | ||
| // Prefer the canonical contract when present; legacy fields stay as fallback | ||
| // during the deprecation period. | ||
| discovered_fields: | ||
| fieldsFromCanonical(state.fields) ?? state.discovered_fields, | ||
| pending_sso_buttons: | ||
| ssoButtonsFromCanonical(state.choices) ?? state.pending_sso_buttons, | ||
| mfa_options: mfaOptionsFromCanonical(state.choices) ?? state.mfa_options, | ||
| sign_in_options: | ||
| signInOptionsFromCanonical(state.choices) ?? state.sign_in_options, | ||
| }; | ||
| } | ||
|
|
||
| export interface ManagedAuthSessionOptions extends ApiClientOptions { | ||
| sessionId: string; | ||
| handoffCode: string; | ||
|
|
@@ -167,7 +299,7 @@ export function useManagedAuthSession( | |
| setSubmitError(null); | ||
| const base = stateRef.current; | ||
| if (!base) return; | ||
| const merged = mergeStateEvent(base, ev); | ||
| const merged = normalizeManagedAuthState(mergeStateEvent(base, ev)); | ||
| stateRef.current = merged; | ||
| setState(merged); | ||
| const nextUI = deriveUIState(merged); | ||
|
|
@@ -214,7 +346,9 @@ export function useManagedAuthSession( | |
| const gen = generationRef.current; | ||
| if (terminalRef.current) return; | ||
| try { | ||
| const fresh = await retrieveManagedAuth(sessionId, t, options); | ||
| const fresh = normalizeManagedAuthState( | ||
| await retrieveManagedAuth(sessionId, t, options), | ||
| ); | ||
| if (gen !== generationRef.current) return; | ||
| if (terminalRef.current) return; | ||
| stateRef.current = fresh; | ||
|
|
@@ -338,7 +472,9 @@ export function useManagedAuthSession( | |
| ); | ||
| if (exchangeRef.current !== ref || !ref.active) return; | ||
| setJwt(token); | ||
| const initial = await retrieveManagedAuth(sessionId, token, options); | ||
| const initial = normalizeManagedAuthState( | ||
| await retrieveManagedAuth(sessionId, token, options), | ||
| ); | ||
| if (exchangeRef.current !== ref || !ref.active) return; | ||
| stateRef.current = initial; | ||
| setState(initial); | ||
|
|
@@ -410,8 +546,12 @@ export function useManagedAuthSession( | |
| const submitFields = useCallback( | ||
| async (credentials: Record<string, string>) => { | ||
| if (!jwt) return; | ||
| const hasCanonicalFields = (stateRef.current?.fields?.length ?? 0) > 0; | ||
| return submit( | ||
| () => submitFieldValues(sessionId, jwt, credentials, options), | ||
| () => | ||
| hasCanonicalFields | ||
| ? submitCanonicalFieldValues(sessionId, jwt, credentials, options) | ||
| : submitFieldValues(sessionId, jwt, credentials, options), | ||
| "Failed to submit credentials", | ||
| ); | ||
| }, | ||
|
|
@@ -422,7 +562,10 @@ export function useManagedAuthSession( | |
| async (button: SSOButton) => { | ||
| if (!jwt) return; | ||
| return submit( | ||
| () => submitSSOButton(sessionId, jwt, button.selector, options), | ||
| () => | ||
| button.id | ||
| ? submitSelectedChoice(sessionId, jwt, button.id, options) | ||
| : submitSSOButton(sessionId, jwt, button.selector, options), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MFA ignores canonical choice submitMedium Severity MFA options built from canonical Additional Locations (1)Reviewed by Cursor Bugbot for commit 30a2d0b. Configure here. |
||
| "Failed to initiate SSO login", | ||
| ); | ||
| }, | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stale inputs on partial SSE
High Severity
The
mergeStateEventandretrieveManagedAuthfunctions now retain previousfields,choices, and legacy awaiting-input arrays when an incoming state event or API response omits them or sendsnull. This can leave outdated forms, MFA, SSO, or sign-in options visible and clickable, even if the server intended to clear them, potentially leading to stale UI and incorrect submissions.Additional Locations (1)
packages/managed-auth-react/src/session/useManagedAuthSession.ts#L159-L173Reviewed by Cursor Bugbot for commit 30a2d0b. Configure here.