From bc9b0a086c372c488899a6a64da4d1ec4c25dd96 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Thu, 25 Jun 2026 15:09:23 -0400 Subject: [PATCH 1/2] 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/2] 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", ); },