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/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 70c851e..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; @@ -56,12 +59,42 @@ export interface SignInOption { description?: string | null; } +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 = + | "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 +115,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..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, @@ -14,8 +16,13 @@ import { import type { AuthErrorPayload, AuthSuccessPayload, + DiscoveredField, + ManagedAuthChoice, + ManagedAuthField, ManagedAuthResponse, MFAType, + MFAOption, + SignInOption, SSOButton, UIState, } from "../lib/types"; @@ -57,6 +64,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 +80,77 @@ 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) => ({ + 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) return null; + 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, + })); +} + +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 +247,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 +294,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 +418,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); @@ -410,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", ); }, @@ -422,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", ); },