import React from 'react'; import { browserSupportsWebAuthn } from '@simplewebauthn/browser'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui'; import { invokeDesktop, isDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; import { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence'; import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence'; import { DesktopHostSwitcherInline } from '@/components/desktop/DesktopHostSwitcher'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { getRuntimeExtraHeadersSync } from '@/lib/runtime-auth'; import { getRuntimeApiBaseUrl, getRuntimeKey, subscribeRuntimeEndpointChanged, switchRuntimeEndpoint } from '@/lib/runtime-switch'; import { desktopHostsGet, desktopHostsSet, getDesktopHostApiUrl, normalizeHostUrl } from '@/lib/desktopHosts'; import { resolveStatusCheckFailureState, runtimeIdentityMatches, type GateState, type RuntimeIdentity } from './sessionAuthGateState'; import { authenticateWithPasskey, cancelPasskeyCeremony, defaultPasskeyStatus, fetchPasskeyStatus, isPasskeyCeremonyAbort, type PasskeyStatus, registerCurrentDevicePasskey, } from '@/lib/passkeys'; const STATUS_CHECK_ENDPOINT = '/auth/session'; // Transient-failure auto-retry for the initial session check. Over the relay the // very first /auth/session can race the tunnel's initial WebSocket attempt (a // failed attempt rejects requests queued on the channel even though the tunnel // immediately reconnects), and on a lossy link the first request can simply drop. // A single-shot check pins the gate on the error screen for a self-healing // condition, so network errors and non-auth server errors (5xx during startup) // retry a bounded number of times before surfacing the error UI. Definitive auth // answers (200/401/429) are never retried. const TRANSIENT_RETRY_MAX_ATTEMPTS = 4; const TRANSIENT_RETRY_BASE_DELAY_MS = 1_500; const TRUST_DEVICE_STORAGE_KEY = 'openchamber.uiAuth.trustDevice'; const LOCAL_DESKTOP_CLIENT_KIND = 'desktop-local'; const LOCAL_DESKTOP_CLIENT_DEDUPE_KEY = 'desktop-local'; const readLocalOrigin = (): string => { if (typeof window === 'undefined') return ''; const injected = (window as typeof window & { __OPENCHAMBER_LOCAL_ORIGIN__?: string }).__OPENCHAMBER_LOCAL_ORIGIN__; return typeof injected === 'string' ? injected.trim() : ''; }; const sameOrigin = (left: string, right: string): boolean => { const normalizedLeft = normalizeHostUrl(left); const normalizedRight = normalizeHostUrl(right); if (!normalizedLeft || !normalizedRight) return false; try { return new URL(normalizedLeft).origin === new URL(normalizedRight).origin; } catch { return false; } }; const shouldIssueDesktopClientToken = (): boolean => { return isDesktopShell(); }; const isLoopbackHostname = (hostname: string): boolean => { const clean = hostname.replace(/^\[|\]$/g, ''); return clean === 'localhost' || clean === '127.0.0.1' || clean === '::1'; }; const isLocalDesktopRuntime = (): boolean => { if (!isDesktopShell()) return false; const localOrigin = readLocalOrigin(); if (!localOrigin) return false; // An empty api base means same-origin requests against the page itself — // which on desktop IS the embedded local server. Requiring an exact origin // match here used to leave local client tokens untagged (no desktop-local // clientKind), and the server's client-create gate then 403'd them. const apiBaseUrl = getRuntimeApiBaseUrl(); const effectiveTarget = apiBaseUrl || (typeof window !== 'undefined' ? window.location.origin : ''); if (sameOrigin(localOrigin, effectiveTarget)) return true; // Loopback aliases (localhost vs 127.0.0.1) still address this machine's // own server. try { const normalized = normalizeHostUrl(effectiveTarget); return Boolean(normalized && isLoopbackHostname(new URL(normalized).hostname)); } catch { return false; } }; const desktopClientAuthMetadata = (): { clientKind?: string; dedupeKey?: string } => { if (!isLocalDesktopRuntime()) return {}; return { clientKind: LOCAL_DESKTOP_CLIENT_KIND, dedupeKey: LOCAL_DESKTOP_CLIENT_DEDUPE_KEY, }; }; const fetchSessionStatus = async (): Promise => { const response = await runtimeFetch(STATUS_CHECK_ENDPOINT, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json', }, }); return response; }; const readStoredTrustDevice = (): boolean => { if (typeof window === 'undefined') { return false; } return window.localStorage.getItem(TRUST_DEVICE_STORAGE_KEY) === 'true'; }; const submitPassword = async (password: string, trustDevice: boolean): Promise => { const issueClientToken = shouldIssueDesktopClientToken(); const response = await runtimeFetch(STATUS_CHECK_ENDPOINT, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ password, trustDevice, issueClientToken, clientLabel: 'OpenChamber Desktop', ...desktopClientAuthMetadata(), }), }); return response; }; const issueDesktopClientToken = async (): Promise => { if (!isDesktopShell()) { return ''; } const response = await runtimeFetch('/api/client-auth/clients', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ label: 'OpenChamber Desktop', ...desktopClientAuthMetadata() }), }).catch(() => null); if (!response?.ok) { return ''; } const payload = await response.json().catch(() => null) as { token?: unknown } | null; return typeof payload?.token === 'string' ? payload.token.trim() : ''; }; const shouldUseDesktopShellPasswordLogin = (): boolean => { return isDesktopShell() && !isLocalDesktopRuntime(); }; const captureRuntimeIdentity = (): RuntimeIdentity => ({ apiBaseUrl: getRuntimeApiBaseUrl(), runtimeKey: getRuntimeKey(), }); const isRuntimeIdentityActive = (identity: RuntimeIdentity): boolean => { return runtimeIdentityMatches(identity, captureRuntimeIdentity()); }; type DesktopPasswordLoginResult = { token: string; status?: number; }; const issueDesktopClientTokenViaShell = async ( password: string, trustDevice: boolean, runtime: RuntimeIdentity, requestHeaders: Record, ): Promise => { if (!isDesktopShell() || typeof window === 'undefined') { return null; } const response = await invokeDesktop('desktop_remote_password_login', { url: runtime.apiBaseUrl, password, trustDevice, requestHeaders, }).catch(() => null); if (!response || typeof response !== 'object') { return null; } const token = (response as { token?: unknown }).token; const status = (response as { status?: unknown }).status; return { token: typeof token === 'string' ? token.trim() : '', ...(typeof status === 'number' ? { status } : {}), }; }; const persistDesktopClientToken = async (runtime: RuntimeIdentity, clientToken: string): Promise => { if (!isDesktopShell() || !clientToken || !isRuntimeIdentityActive(runtime)) return false; const cfg = await desktopHostsGet().catch(() => null); if (!cfg || !isRuntimeIdentityActive(runtime)) return false; if (cfg.localOrigin && sameOrigin(cfg.localOrigin, runtime.apiBaseUrl)) { await desktopHostsSet({ hosts: cfg.hosts, defaultHostId: cfg.defaultHostId, initialHostChoiceCompleted: cfg.initialHostChoiceCompleted, localClientToken: clientToken, }).catch(() => undefined); return isRuntimeIdentityActive(runtime); } let changed = false; const hosts = cfg.hosts.map((host) => { if (!sameOrigin(getDesktopHostApiUrl(host), runtime.apiBaseUrl)) { return host; } if (host.clientToken === clientToken) { return host; } changed = true; return { ...host, clientToken }; }); if (!changed) return true; if (!isRuntimeIdentityActive(runtime)) return false; await desktopHostsSet({ hosts, defaultHostId: cfg.defaultHostId, initialHostChoiceCompleted: cfg.initialHostChoiceCompleted, }).catch(() => undefined); return isRuntimeIdentityActive(runtime); }; const applyDesktopClientToken = async ( clientToken: string, runtime: RuntimeIdentity, requestHeaders: Record, ): Promise => { if (!clientToken || !isRuntimeIdentityActive(runtime)) return false; if (!await persistDesktopClientToken(runtime, clientToken)) return false; if (!isRuntimeIdentityActive(runtime)) return false; switchRuntimeEndpoint({ apiBaseUrl: runtime.apiBaseUrl, clientToken, requestHeaders: Object.keys(requestHeaders).length > 0 ? requestHeaders : null, runtimeKey: runtime.runtimeKey, }); return true; }; const AuthShell: React.FC<{ children: React.ReactNode }> = ({ children }) => { const titlebarDragStyle = React.useMemo(() => { return { height: 'var(--oc-wco-titlebar-height, 0px)', right: 'var(--oc-wco-right-inset, 0px)', }; }, []); return (
{children}
); }; const LoadingScreen: React.FC = () => (
); const ErrorScreen: React.FC = ({ onRetry, errorType = 'network', retryAfter, children }) => { const { t } = useI18n(); const isRateLimit = errorType === 'rate-limit'; const minutes = retryAfter ? Math.ceil(retryAfter / 60) : 1; return (

{isRateLimit ? t('sessionAuth.error.rateLimitTitle') : t('sessionAuth.error.networkTitle')}

{isRateLimit ? (minutes > 1 ? t('sessionAuth.error.rateLimitDescriptionPlural', { minutes }) : t('sessionAuth.error.rateLimitDescriptionSingle', { minutes })) : t('sessionAuth.error.networkDescription')}

{children}
); }; interface SessionAuthGateProps { children: React.ReactNode; } interface ErrorScreenProps { onRetry: () => void; errorType?: 'network' | 'rate-limit'; retryAfter?: number; children?: React.ReactNode; } export const SessionAuthGate: React.FC = ({ children, }) => { const { t } = useI18n(); const vscodeRuntime = React.useMemo(() => isVSCodeRuntime(), []); const skipAuth = vscodeRuntime; const showHostSwitcher = React.useMemo(() => isDesktopShell() && !vscodeRuntime, [vscodeRuntime]); const [state, setState] = React.useState(() => (skipAuth ? 'authenticated' : 'pending')); const [password, setPassword] = React.useState(''); const [isSubmitting, setIsSubmitting] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); const [retryAfter, setRetryAfter] = React.useState(undefined); const [isTunnelLocked, setIsTunnelLocked] = React.useState(false); const [passkeyStatus, setPasskeyStatus] = React.useState(defaultPasskeyStatus); const [supportsPasskeys, setSupportsPasskeys] = React.useState(false); const [isPasskeyBusy, setIsPasskeyBusy] = React.useState(false); const [trustDevice, setTrustDevice] = React.useState(() => readStoredTrustDevice()); const [activePasskeyAction, setActivePasskeyAction] = React.useState<'auth' | 'register' | null>(null); const passwordInputRef = React.useRef(null); const hasResyncedRef = React.useRef(skipAuth); React.useEffect(() => { if (typeof window === 'undefined') { return; } window.localStorage.setItem(TRUST_DEVICE_STORAGE_KEY, trustDevice ? 'true' : 'false'); }, [trustDevice]); const refreshPasskeyStatus = React.useCallback(async (runtime = captureRuntimeIdentity()) => { if (skipAuth) { return defaultPasskeyStatus; } try { const nextStatus = await fetchPasskeyStatus(); if (isRuntimeIdentityActive(runtime)) { setPasskeyStatus(nextStatus); } return nextStatus; } catch { if (isRuntimeIdentityActive(runtime)) { setPasskeyStatus(defaultPasskeyStatus); } return defaultPasskeyStatus; } }, [skipAuth]); React.useEffect(() => { let cancelled = false; if (skipAuth) { return; } void (async () => { try { if (!window.isSecureContext || !browserSupportsWebAuthn()) { if (!cancelled) { setSupportsPasskeys(false); } return; } if (!cancelled) { setSupportsPasskeys(true); } } catch { if (!cancelled) { setSupportsPasskeys(false); } } })(); return () => { cancelled = true; }; }, [skipAuth]); // Bounded retry scheduling for transient session-check failures. Lives in refs // so retries survive re-renders; the timer is cleared on unmount, endpoint // switch, and any definitive server answer. const transientRetryAttemptRef = React.useRef(0); const transientRetryTimerRef = React.useRef(null); const checkStatusRef = React.useRef<(() => Promise) | null>(null); const clearTransientRetry = React.useCallback(() => { if (transientRetryTimerRef.current !== null) { window.clearTimeout(transientRetryTimerRef.current); transientRetryTimerRef.current = null; } }, []); const resetTransientRetry = React.useCallback(() => { transientRetryAttemptRef.current = 0; clearTransientRetry(); }, [clearTransientRetry]); // Returns true when another attempt was scheduled (caller keeps the pending // UI); false when the retry budget is exhausted (caller shows the error UI). const scheduleTransientRetry = React.useCallback((): boolean => { if (transientRetryAttemptRef.current >= TRANSIENT_RETRY_MAX_ATTEMPTS) return false; transientRetryAttemptRef.current += 1; clearTransientRetry(); transientRetryTimerRef.current = window.setTimeout(() => { transientRetryTimerRef.current = null; void checkStatusRef.current?.(); }, TRANSIENT_RETRY_BASE_DELAY_MS * transientRetryAttemptRef.current); return true; }, [clearTransientRetry]); React.useEffect(() => clearTransientRetry, [clearTransientRetry]); const checkStatus = React.useCallback(async () => { if (skipAuth) { setState('authenticated'); return; } const runtime = captureRuntimeIdentity(); setState((prev) => (prev === 'authenticated' ? prev : 'pending')); try { const [response, latestPasskeyStatus] = await Promise.all([ fetchSessionStatus(), refreshPasskeyStatus(runtime), ]); const responseText = await response.text(); if (!isRuntimeIdentityActive(runtime)) { return; } if (response.ok) { resetTransientRetry(); setState('authenticated'); setIsTunnelLocked(false); setErrorMessage(''); setRetryAfter(undefined); return; } if (response.status === 401) { let data: { tunnelLocked?: boolean; debug?: { hasRefreshToken: boolean; message: string } } = {}; try { data = JSON.parse(responseText); } catch { data = {}; } resetTransientRetry(); setIsTunnelLocked(data.tunnelLocked === true); setPasskeyStatus(latestPasskeyStatus); setState('locked'); setRetryAfter(undefined); return; } if (response.status === 429) { let data: { retryAfter?: number } = {}; try { data = JSON.parse(responseText); } catch { data = {}; } resetTransientRetry(); setRetryAfter(data.retryAfter); setIsTunnelLocked(false); setState('rate-limited'); return; } // Non-auth server error (e.g. 502/503 while the backend is still coming // up) — transient; keep the pending UI and retry before surfacing. if (scheduleTransientRetry()) return; setState('error'); setIsTunnelLocked(false); } catch (error) { if (!isRuntimeIdentityActive(runtime)) { return; } console.warn('Failed to check session status:', error); if (resolveStatusCheckFailureState({ shouldUseDesktopShellPasswordLogin: shouldUseDesktopShellPasswordLogin() }) === 'locked') { setState('locked'); setRetryAfter(undefined); setIsTunnelLocked(false); return; } // Network-level failure — over the relay this is typically the initial // tunnel attempt racing this request; it self-heals within seconds. if (scheduleTransientRetry()) return; setState('error'); setIsTunnelLocked(false); } }, [refreshPasskeyStatus, resetTransientRetry, scheduleTransientRetry, skipAuth]); React.useEffect(() => { checkStatusRef.current = checkStatus; }, [checkStatus]); React.useEffect(() => { if (skipAuth) { return; } void checkStatus(); }, [checkStatus, skipAuth]); React.useEffect(() => { if (skipAuth) { return; } return subscribeRuntimeEndpointChanged(() => { cancelPasskeyCeremony(); setPassword(''); setErrorMessage(''); setRetryAfter(undefined); setIsTunnelLocked(false); setIsSubmitting(false); setActivePasskeyAction(null); setIsPasskeyBusy(false); resetTransientRetry(); setState('pending'); void checkStatus(); }); }, [checkStatus, resetTransientRetry, skipAuth]); React.useEffect(() => { if (!skipAuth && state === 'locked') { hasResyncedRef.current = false; } }, [skipAuth, state]); React.useEffect(() => { if (state === 'locked' && passwordInputRef.current) { passwordInputRef.current.focus(); passwordInputRef.current.select(); } }, [state]); React.useEffect(() => { if (skipAuth) { return; } if (state === 'authenticated' && !hasResyncedRef.current) { hasResyncedRef.current = true; void (async () => { await initializeAppearancePreferences(); await syncDesktopSettings(); await applyPersistedDirectoryPreferences(); })(); } }, [skipAuth, state]); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); await handlePasswordUnlock(false); }; const registerPasskeyForCurrentSession = React.useCallback(async () => { const runtime = captureRuntimeIdentity(); setActivePasskeyAction('register'); setIsPasskeyBusy(true); try { await registerCurrentDevicePasskey(); } finally { if (isRuntimeIdentityActive(runtime)) { setActivePasskeyAction(null); setIsPasskeyBusy(false); } } if (!isRuntimeIdentityActive(runtime)) return; await refreshPasskeyStatus(runtime); }, [refreshPasskeyStatus]); const cancelActivePasskey = React.useCallback(() => { cancelPasskeyCeremony(); setActivePasskeyAction(null); setIsPasskeyBusy(false); }, []); const handlePasswordUnlock = React.useCallback(async (enrollPasskey: boolean) => { if (isTunnelLocked) { return; } if (!password || isSubmitting) { return; } if (isPasskeyBusy) { cancelActivePasskey(); } const runtime = captureRuntimeIdentity(); const requestHeaders = getRuntimeExtraHeadersSync(); setIsSubmitting(true); setErrorMessage(''); try { if (shouldUseDesktopShellPasswordLogin()) { const shellLogin = await issueDesktopClientTokenViaShell(password, trustDevice, runtime, requestHeaders); if (!isRuntimeIdentityActive(runtime)) return; if (shellLogin?.token) { setPassword(''); setIsTunnelLocked(false); if (!await applyDesktopClientToken(shellLogin.token, runtime, requestHeaders)) return; setState('authenticated'); return; } if (shellLogin?.status === 401) { setErrorMessage(t('sessionAuth.error.incorrectPassword')); setIsTunnelLocked(false); setState('locked'); return; } if (shellLogin?.status === 429) { setRetryAfter(undefined); setIsTunnelLocked(false); setState('rate-limited'); return; } } const response = await submitPassword(password, trustDevice); if (!isRuntimeIdentityActive(runtime)) return; if (response.ok) { const payload = await response.json().catch(() => null) as { clientToken?: unknown } | null; if (!isRuntimeIdentityActive(runtime)) return; const shouldUseClientToken = shouldIssueDesktopClientToken(); let clientToken = ''; if (shouldUseClientToken) { clientToken = typeof payload?.clientToken === 'string' && payload.clientToken.trim() ? payload.clientToken.trim() : ''; if (!clientToken) { const shellLogin = await issueDesktopClientTokenViaShell(password, trustDevice, runtime, requestHeaders); if (!isRuntimeIdentityActive(runtime)) return; clientToken = shellLogin?.token || await issueDesktopClientToken(); if (!isRuntimeIdentityActive(runtime)) return; } } setPassword(''); setIsTunnelLocked(false); if (clientToken) { if (!await applyDesktopClientToken(clientToken, runtime, requestHeaders)) return; } if (enrollPasskey && supportsPasskeys) { try { await registerPasskeyForCurrentSession(); if (!isRuntimeIdentityActive(runtime)) return; toast.success(t('sessionAuth.toast.passkeyAdded')); setState('authenticated'); return; } catch (error) { if (isPasskeyCeremonyAbort(error)) { toast.message(t('sessionAuth.toast.passkeySetupCanceled')); } else { const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySetupFailed'); toast.error(message); } setState('authenticated'); return; } } setState('authenticated'); return; } if (response.status === 401) { setErrorMessage(t('sessionAuth.error.incorrectPassword')); setIsTunnelLocked(false); setState('locked'); return; } if (response.status === 429) { const data = await response.json().catch(() => ({})); setRetryAfter(data.retryAfter); setIsTunnelLocked(false); setState('rate-limited'); return; } setErrorMessage(t('sessionAuth.error.unexpectedResponse')); setIsTunnelLocked(false); setState('error'); } catch (error) { if (!isRuntimeIdentityActive(runtime)) return; console.warn('Failed to submit UI password:', error); const shellLogin = shouldUseDesktopShellPasswordLogin() ? await issueDesktopClientTokenViaShell(password, trustDevice, runtime, requestHeaders) : null; if (!isRuntimeIdentityActive(runtime)) return; if (shellLogin?.token) { setPassword(''); setIsTunnelLocked(false); if (!await applyDesktopClientToken(shellLogin.token, runtime, requestHeaders)) return; setState('authenticated'); return; } if (shellLogin?.status === 401) { setErrorMessage(t('sessionAuth.error.incorrectPassword')); setIsTunnelLocked(false); setState('locked'); return; } if (shellLogin?.status === 429) { setRetryAfter(undefined); setIsTunnelLocked(false); setState('rate-limited'); return; } setErrorMessage(t('sessionAuth.error.networkRetry')); setIsTunnelLocked(false); setState('error'); } finally { if (isRuntimeIdentityActive(runtime)) { setIsSubmitting(false); } } }, [cancelActivePasskey, isPasskeyBusy, isSubmitting, isTunnelLocked, password, registerPasskeyForCurrentSession, supportsPasskeys, t, trustDevice]); const handlePasskeyUnlock = React.useCallback(async () => { if (isSubmitting || !supportsPasskeys) { return; } if (isPasskeyBusy) { cancelActivePasskey(); return; } setIsPasskeyBusy(true); setActivePasskeyAction('auth'); setErrorMessage(''); const runtime = captureRuntimeIdentity(); const requestHeaders = getRuntimeExtraHeadersSync(); try { const payload = await authenticateWithPasskey(trustDevice, { issueClientToken: shouldIssueDesktopClientToken(), clientLabel: 'OpenChamber Desktop', ...desktopClientAuthMetadata(), }) as { clientToken?: unknown } | null; const clientToken = shouldIssueDesktopClientToken() && typeof payload?.clientToken === 'string' && payload.clientToken.trim() ? payload.clientToken.trim() : ''; if (!isRuntimeIdentityActive(runtime)) return; if (clientToken) { if (!await applyDesktopClientToken(clientToken, runtime, requestHeaders)) return; } setPassword(''); setState('authenticated'); } catch (error) { if (!isRuntimeIdentityActive(runtime)) return; if (isPasskeyCeremonyAbort(error)) { setErrorMessage(''); } else { const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySignInCanceled'); setErrorMessage(message); } } finally { if (isRuntimeIdentityActive(runtime)) { setActivePasskeyAction(null); setIsPasskeyBusy(false); } } }, [cancelActivePasskey, isPasskeyBusy, isSubmitting, supportsPasskeys, t, trustDevice]); const handlePasskeySetupOnly = React.useCallback(async () => { if (isSubmitting || isTunnelLocked || !supportsPasskeys) { return; } if (isPasskeyBusy) { cancelActivePasskey(); return; } if (state !== 'authenticated') { if (!password) { setErrorMessage(t('sessionAuth.error.enterPasswordForPasskey')); return; } await handlePasswordUnlock(true); return; } setErrorMessage(''); try { await registerPasskeyForCurrentSession(); toast.success(t('sessionAuth.toast.passkeyAdded')); } catch (error) { if (isPasskeyCeremonyAbort(error)) { toast.message(t('sessionAuth.toast.passkeySetupCanceled')); return; } const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySetupFailed'); toast.error(message); } }, [cancelActivePasskey, handlePasswordUnlock, isPasskeyBusy, isSubmitting, isTunnelLocked, password, registerPasskeyForCurrentSession, state, supportsPasskeys, t]); const canOfferPasskeySetup = supportsPasskeys && passkeyStatus.enabled; const canUsePasskey = canOfferPasskeySetup && passkeyStatus.hasPasskeys; if (state === 'pending') { return ; } if (state === 'error') { return ( { resetTransientRetry(); void checkStatus(); }} errorType="network"> {showHostSwitcher && (

{t('sessionAuth.locked.hostSwitcherHint')}

)}
); } if (state === 'rate-limited') { return void checkStatus()} errorType="rate-limit" retryAfter={retryAfter} />; } if (state === 'locked') { return (

{isTunnelLocked ? t('sessionAuth.locked.tunnelTitle') : t('sessionAuth.locked.unlockTitle')}

{isTunnelLocked ? t('sessionAuth.locked.tunnelDescription') : t('sessionAuth.locked.passwordDescription')}

{!isTunnelLocked && (
{canUsePasskey && ( )}
{ setPassword(event.target.value); if (errorMessage) { setErrorMessage(''); } }} className="pl-10" aria-invalid={Boolean(errorMessage) || undefined} aria-describedby={errorMessage ? 'oc-ui-auth-error' : undefined} disabled={isSubmitting} />
{canOfferPasskeySetup ? (
) : ( )} {errorMessage && (

{errorMessage}

)}
)} {showHostSwitcher && (

{t('sessionAuth.locked.hostSwitcherHint')}

)}
); } return <>{children}; };