From bc9b0a086c372c488899a6a64da4d1ec4c25dd96 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Thu, 25 Jun 2026 15:09:23 -0400 Subject: [PATCH 1/4] feat: prefer canonical awaiting-input contract Read top-level fields/choices from managed-auth state events and responses when present, mapping them into the existing form rendering model. Keep falling back to legacy discovered_fields/pending_sso_buttons/mfa_options/sign_in_options so older API responses remain supported during the deprecation window. Co-authored-by: Cursor --- .changeset/canonical-awaiting-input.md | 5 ++ packages/managed-auth-react/src/lib/types.ts | 30 +++++++ .../src/session/useManagedAuthSession.ts | 81 ++++++++++++++++++- 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 .changeset/canonical-awaiting-input.md diff --git a/.changeset/canonical-awaiting-input.md b/.changeset/canonical-awaiting-input.md new file mode 100644 index 0000000..130a0a3 --- /dev/null +++ b/.changeset/canonical-awaiting-input.md @@ -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. diff --git a/packages/managed-auth-react/src/lib/types.ts b/packages/managed-auth-react/src/lib/types.ts index 70c851e..8d04fef 100644 --- a/packages/managed-auth-react/src/lib/types.ts +++ b/packages/managed-auth-react/src/lib/types.ts @@ -56,12 +56,40 @@ export interface SignInOption { description?: string | null; } +export interface ManagedAuthField { + ref: string; + type: "identifier" | "password" | "code" | "totp_code" | "totp_secret" | "text"; + label?: string; + required?: boolean; +} + +export type ManagedAuthChoiceType = + | "mfa_method" + | "sso_provider" + | "sign_in_method" + | "auth_method" + | "identifier_method" + | "account" + | "other"; + +export interface ManagedAuthChoice { + id: string; + type: ManagedAuthChoiceType; + label: string; + description?: string | null; + observed_selector?: string | null; + display_text?: string | null; + context?: string | null; +} + export interface ManagedAuthStateEventData { event: "managed_auth_state"; timestamp: string; flow_status: FlowStatus; flow_step: FlowStep; flow_type?: "LOGIN" | "REAUTH"; + fields?: ManagedAuthField[]; + choices?: ManagedAuthChoice[]; discovered_fields?: DiscoveredField[]; mfa_options?: MFAOption[]; sign_in_options?: SignInOption[]; @@ -82,6 +110,8 @@ export interface ManagedAuthResponse { flow_status: FlowStatus; flow_step: FlowStep; flow_type?: "LOGIN" | "REAUTH" | null; + fields?: ManagedAuthField[] | null; + choices?: ManagedAuthChoice[] | null; discovered_fields?: DiscoveredField[] | null; pending_sso_buttons?: SSOButton[] | null; mfa_options?: MFAOption[] | null; diff --git a/packages/managed-auth-react/src/session/useManagedAuthSession.ts b/packages/managed-auth-react/src/session/useManagedAuthSession.ts index 1e817c7..6162515 100644 --- a/packages/managed-auth-react/src/session/useManagedAuthSession.ts +++ b/packages/managed-auth-react/src/session/useManagedAuthSession.ts @@ -14,8 +14,13 @@ import { import type { AuthErrorPayload, AuthSuccessPayload, + DiscoveredField, + ManagedAuthChoice, + ManagedAuthField, ManagedAuthResponse, MFAType, + MFAOption, + SignInOption, SSOButton, UIState, } from "../lib/types"; @@ -57,6 +62,8 @@ function mergeStateEvent( flow_status: ev.flow_status, flow_step: ev.flow_step, flow_type: ev.flow_type ?? base.flow_type ?? null, + fields: ev.fields ?? null, + choices: ev.choices ?? null, discovered_fields: ev.discovered_fields ?? null, pending_sso_buttons: ev.pending_sso_buttons ?? null, mfa_options: ev.mfa_options ?? null, @@ -71,6 +78,74 @@ 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) return null; + return fields.map((field) => ({ + name: field.ref, + type: fieldTypeToDiscoveredType(field), + label: field.label || field.ref, + required: field.required ?? true, + })); +} + +function ssoButtonsFromCanonical(choices?: ManagedAuthChoice[] | null): SSOButton[] | null { + if (!choices) return null; + return choices + .filter((choice) => choice.type === "sso_provider") + .map((choice) => ({ + provider: choice.id, + selector: choice.observed_selector || choice.id, + label: choice.label, + })); +} + +function mfaOptionsFromCanonical(choices?: ManagedAuthChoice[] | null): MFAOption[] | null { + if (!choices) return null; + return choices + .filter((choice) => choice.type === "mfa_method") + .map((choice) => ({ + type: choice.id as MFAType, + label: choice.label, + description: choice.description ?? undefined, + })); +} + +function signInOptionsFromCanonical(choices?: ManagedAuthChoice[] | null): SignInOption[] | null { + if (!choices) return null; + return 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, + })); +} + +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 +242,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 +289,7 @@ 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 +413,7 @@ 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); From 4aa184fcb9c91ba16c5ae113225d7b4d4288a830 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Thu, 25 Jun 2026 16:35:06 -0400 Subject: [PATCH 2/4] feat: submit canonical awaiting input When canonical fields/choices are present, submit field_values by canonical field ID and selected_choice_id by canonical choice ID. Continue supporting legacy fields, SSO selectors, MFA option IDs, and sign-in option IDs when canonical data is absent. Co-authored-by: Cursor --- packages/managed-auth-react/src/lib/api.ts | 28 ++++++++++++++++--- packages/managed-auth-react/src/lib/types.ts | 5 ++++ .../src/session/useManagedAuthSession.ts | 18 ++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/managed-auth-react/src/lib/api.ts b/packages/managed-auth-react/src/lib/api.ts index 1d995d1..15f953a 100644 --- a/packages/managed-auth-react/src/lib/api.ts +++ b/packages/managed-auth-react/src/lib/api.ts @@ -92,7 +92,9 @@ export async function retrieveManagedAuth( } interface SubmitBody { - fields: Record; + fields?: Record; + field_values?: Record; + selected_choice_id?: string; sso_button_selector?: string; mfa_option_id?: MFAType; sign_in_option_id?: string; @@ -128,6 +130,24 @@ export function submitFieldValues( return submit(id, jwt, { fields }, options); } +export function submitCanonicalFieldValues( + id: string, + jwt: string, + fieldValues: Record, + options?: ApiClientOptions, +): Promise { + return submit(id, jwt, { field_values: fieldValues }, options); +} + +export function submitSelectedChoice( + id: string, + jwt: string, + selectedChoiceId: string, + options?: ApiClientOptions, +): Promise { + return submit(id, jwt, { selected_choice_id: selectedChoiceId }, options); +} + export function submitSSOButton( id: string, jwt: string, @@ -137,7 +157,7 @@ export function submitSSOButton( return submit( id, jwt, - { fields: {}, sso_button_selector: selector }, + { sso_button_selector: selector }, options, ); } @@ -148,7 +168,7 @@ export function submitMFASelection( mfaType: MFAType, options?: ApiClientOptions, ): Promise { - return submit(id, jwt, { fields: {}, mfa_option_id: mfaType }, options); + return submit(id, jwt, { mfa_option_id: mfaType }, options); } export function submitSignInOption( @@ -160,7 +180,7 @@ export function submitSignInOption( return submit( id, jwt, - { fields: {}, sign_in_option_id: signInOptionId }, + { sign_in_option_id: signInOptionId }, options, ); } diff --git a/packages/managed-auth-react/src/lib/types.ts b/packages/managed-auth-react/src/lib/types.ts index 8d04fef..deb39ad 100644 --- a/packages/managed-auth-react/src/lib/types.ts +++ b/packages/managed-auth-react/src/lib/types.ts @@ -28,6 +28,8 @@ export type MFAType = | "other"; export interface DiscoveredField { + id?: string; + ref?: string; name: string; label: string; type: "text" | "email" | "password" | "tel" | "code" | "totp"; @@ -38,6 +40,7 @@ export interface DiscoveredField { } export interface SSOButton { + id?: string; provider: string; selector: string; label?: string; @@ -57,10 +60,12 @@ export interface SignInOption { } export interface ManagedAuthField { + id: string; ref: string; type: "identifier" | "password" | "code" | "totp_code" | "totp_secret" | "text"; label?: string; required?: boolean; + observed_selector?: string | null; } export type ManagedAuthChoiceType = diff --git a/packages/managed-auth-react/src/session/useManagedAuthSession.ts b/packages/managed-auth-react/src/session/useManagedAuthSession.ts index 6162515..8c39e9e 100644 --- a/packages/managed-auth-react/src/session/useManagedAuthSession.ts +++ b/packages/managed-auth-react/src/session/useManagedAuthSession.ts @@ -5,7 +5,9 @@ import { retrieveManagedAuth, streamManagedAuthEvents, submitFieldValues, + submitCanonicalFieldValues, submitMFASelection, + submitSelectedChoice, submitSignInOption, submitSSOButton, type ApiClientOptions, @@ -94,7 +96,9 @@ function fieldTypeToDiscoveredType(field: ManagedAuthField): DiscoveredField["ty function fieldsFromCanonical(fields?: ManagedAuthField[] | null): DiscoveredField[] | null { if (!fields) return null; return fields.map((field) => ({ - name: field.ref, + id: field.id, + ref: field.ref, + name: field.id, type: fieldTypeToDiscoveredType(field), label: field.label || field.ref, required: field.required ?? true, @@ -106,6 +110,7 @@ function ssoButtonsFromCanonical(choices?: ManagedAuthChoice[] | null): SSOButto return choices .filter((choice) => choice.type === "sso_provider") .map((choice) => ({ + id: choice.id, provider: choice.id, selector: choice.observed_selector || choice.id, label: choice.label, @@ -485,8 +490,12 @@ export function useManagedAuthSession( const submitFields = useCallback( async (credentials: Record) => { 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", ); }, @@ -497,7 +506,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), "Failed to initiate SSO login", ); }, From 30a2d0b4bff813d3a939d46b09c80fb0bb6f5457 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Tue, 30 Jun 2026 16:46:06 -0400 Subject: [PATCH 3/4] fix: preserve canonical awaiting input state Co-authored-by: Cursor --- packages/managed-auth-react/src/lib/api.ts | 14 +--- packages/managed-auth-react/src/lib/types.ts | 8 +- .../src/session/useManagedAuthSession.ts | 79 +++++++++++++------ 3 files changed, 62 insertions(+), 39 deletions(-) diff --git a/packages/managed-auth-react/src/lib/api.ts b/packages/managed-auth-react/src/lib/api.ts index 15f953a..105cba5 100644 --- a/packages/managed-auth-react/src/lib/api.ts +++ b/packages/managed-auth-react/src/lib/api.ts @@ -154,12 +154,7 @@ export function submitSSOButton( selector: string, options?: ApiClientOptions, ): Promise { - return submit( - id, - jwt, - { sso_button_selector: selector }, - options, - ); + return submit(id, jwt, { sso_button_selector: selector }, options); } export function submitMFASelection( @@ -177,12 +172,7 @@ export function submitSignInOption( signInOptionId: string, options?: ApiClientOptions, ): Promise { - return submit( - id, - jwt, - { sign_in_option_id: signInOptionId }, - options, - ); + return submit(id, jwt, { sign_in_option_id: signInOptionId }, options); } /** Callbacks for the SSE event stream. */ diff --git a/packages/managed-auth-react/src/lib/types.ts b/packages/managed-auth-react/src/lib/types.ts index deb39ad..85f13b3 100644 --- a/packages/managed-auth-react/src/lib/types.ts +++ b/packages/managed-auth-react/src/lib/types.ts @@ -62,7 +62,13 @@ export interface SignInOption { export interface ManagedAuthField { id: string; ref: string; - type: "identifier" | "password" | "code" | "totp_code" | "totp_secret" | "text"; + type: + | "identifier" + | "password" + | "code" + | "totp_code" + | "totp_secret" + | "text"; label?: string; required?: boolean; observed_selector?: string | null; diff --git a/packages/managed-auth-react/src/session/useManagedAuthSession.ts b/packages/managed-auth-react/src/session/useManagedAuthSession.ts index 8c39e9e..f5acbd0 100644 --- a/packages/managed-auth-react/src/session/useManagedAuthSession.ts +++ b/packages/managed-auth-react/src/session/useManagedAuthSession.ts @@ -64,12 +64,13 @@ function mergeStateEvent( flow_status: ev.flow_status, flow_step: ev.flow_step, flow_type: ev.flow_type ?? base.flow_type ?? null, - fields: ev.fields ?? null, - choices: ev.choices ?? 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, external_action_message: ev.external_action_message ?? null, website_error: ev.website_error ?? null, error_message: ev.error_message ?? null, @@ -80,7 +81,9 @@ function mergeStateEvent( }; } -function fieldTypeToDiscoveredType(field: ManagedAuthField): DiscoveredField["type"] { +function fieldTypeToDiscoveredType( + field: ManagedAuthField, +): DiscoveredField["type"] { switch (field.type) { case "identifier": return "email"; @@ -93,8 +96,10 @@ function fieldTypeToDiscoveredType(field: ManagedAuthField): DiscoveredField["ty } } -function fieldsFromCanonical(fields?: ManagedAuthField[] | null): DiscoveredField[] | null { - if (!fields) return null; +function fieldsFromCanonical( + fields?: ManagedAuthField[] | null, +): DiscoveredField[] | null { + if (!fields?.length) return null; return fields.map((field) => ({ id: field.id, ref: field.ref, @@ -105,9 +110,11 @@ function fieldsFromCanonical(fields?: ManagedAuthField[] | null): DiscoveredFiel })); } -function ssoButtonsFromCanonical(choices?: ManagedAuthChoice[] | null): SSOButton[] | null { - if (!choices) return null; - return choices +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, @@ -115,39 +122,55 @@ function ssoButtonsFromCanonical(choices?: ManagedAuthChoice[] | null): SSOButto selector: choice.observed_selector || choice.id, label: choice.label, })); + return buttons.length ? buttons : null; } -function mfaOptionsFromCanonical(choices?: ManagedAuthChoice[] | null): MFAOption[] | null { - if (!choices) return null; - return choices +function mfaOptionsFromCanonical( + choices?: ManagedAuthChoice[] | null, +): MFAOption[] | null { + if (!choices?.length) return null; + const options = choices .filter((choice) => choice.type === "mfa_method") .map((choice) => ({ type: choice.id as MFAType, label: choice.label, description: choice.description ?? undefined, })); + return options.length ? options : null; } -function signInOptionsFromCanonical(choices?: ManagedAuthChoice[] | null): SignInOption[] | null { - if (!choices) return null; - return choices - .filter((choice) => choice.type !== "sso_provider" && choice.type !== "mfa_method") +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, + description: + choice.description ?? choice.context ?? choice.display_text ?? null, })); + return options.length ? options : null; } -function normalizeManagedAuthState(state: ManagedAuthResponse): ManagedAuthResponse { +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, + 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, + sign_in_options: + signInOptionsFromCanonical(state.choices) ?? state.sign_in_options, }; } @@ -294,7 +317,9 @@ export function useManagedAuthSession( const gen = generationRef.current; if (terminalRef.current) return; try { - const fresh = normalizeManagedAuthState(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; @@ -418,7 +443,9 @@ export function useManagedAuthSession( ); if (exchangeRef.current !== ref || !ref.active) return; setJwt(token); - const initial = normalizeManagedAuthState(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); From a59c8d1d224dc4444c25278184f2e52e572417ca Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Wed, 1 Jul 2026 11:17:12 -0400 Subject: [PATCH 4/4] fix: normalize canonical MFA choices Co-authored-by: Cursor --- .../src/session/useManagedAuthSession.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/managed-auth-react/src/session/useManagedAuthSession.ts b/packages/managed-auth-react/src/session/useManagedAuthSession.ts index f5acbd0..ea12c7e 100644 --- a/packages/managed-auth-react/src/session/useManagedAuthSession.ts +++ b/packages/managed-auth-react/src/session/useManagedAuthSession.ts @@ -125,6 +125,35 @@ function ssoButtonsFromCanonical( 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 { @@ -132,7 +161,7 @@ function mfaOptionsFromCanonical( const options = choices .filter((choice) => choice.type === "mfa_method") .map((choice) => ({ - type: choice.id as MFAType, + type: normalizeMFAChoiceId(choice.id), label: choice.label, description: choice.description ?? undefined, }));