inspector/core/react/useEmaIdpLoginState.ts
cliffhall ba48e06ffd fix(auth): guard OAuth-storage async paths against unhandled rejections
Review follow-ups on the RemoteOAuthStorage web migration:

- useEmaIdpLoginState.logout(): add a .catch to the now-async
  clearEmaIdpSession call so a storage failure no longer surfaces as an
  unhandled rejection; leave loginState unchanged on failure (the session
  was not actually cleared). Add a test asserting no unhandled rejection.
- createOAuthStore: attach a no-op catch to the eager persistLoadPromise
  so a hydration read that fails before any load() awaits it does not
  become an unhandled rejection; real awaiters still observe the rejection
  via the shared reference.
- storage-remote.test: replace `as unknown as typeof fetch` double-casts
  with the typed vi.fn<typeof fetch>() pattern the same PR established.
- remoteOAuthStorage: correct the doc comment to describe the cache-of-one
  accurately instead of implying a keyed map.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:59:40 -04:00

57 lines
1.7 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import type { OAuthStorage } from "../auth/storage.js";
import {
clearEmaIdpSession,
getEmaIdpLoginState,
normalizeIdpIssuer,
type EmaIdpLoginState,
} from "../auth/ema/idpSession.js";
export interface UseEmaIdpLoginStateResult {
loginState: EmaIdpLoginState;
refresh: () => Promise<void>;
logout: () => void;
}
/**
* Read and clear EMA IdP session state for Client Settings UX.
* Pass `active: true` while the settings surface is open to refresh on open.
*/
export function useEmaIdpLoginState(
storage: OAuthStorage,
issuer: string | undefined,
active: boolean,
): UseEmaIdpLoginStateResult {
const normalizedIssuer = issuer ? normalizeIdpIssuer(issuer) : "";
const [loginState, setLoginState] = useState<EmaIdpLoginState>("none");
const refresh = useCallback(async () => {
if (!normalizedIssuer) {
setLoginState("none");
return;
}
setLoginState(await getEmaIdpLoginState(storage, normalizedIssuer));
}, [storage, normalizedIssuer]);
useEffect(() => {
if (!active) return;
void refresh();
}, [active, refresh]);
const logout = useCallback(() => {
if (!normalizedIssuer) return;
void clearEmaIdpSession(storage, normalizedIssuer)
.then(() => {
setLoginState("none");
})
.catch(() => {
// Clearing the persisted IdP session failed (e.g. the storage backend
// is unreachable). Swallow the rejection so it does not surface as an
// unhandled promise, and leave loginState unchanged so the UI keeps
// reflecting the still-present session rather than falsely showing
// signed-out.
});
}, [storage, normalizedIssuer]);
return { loginState, refresh, logout };
}