import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactElement, type ReactNode, } from 'react'; import type { ZyphrClient } from '@zyphr-dev/node-sdk'; import { SessionManager, type LoginParams, type SessionState, type TokenStorage, } from '@zyphr-dev/auth-core'; import { createZyphrTransport } from './transport.js'; import { createWebStorage } from './storage.js'; export interface ZyphrAuthProviderProps { /** A client-safe ZyphrClient (constructed with your publishable application key). */ client: ZyphrClient; /** Override token storage. Defaults to localStorage (with an in-memory fallback). */ storage?: TokenStorage; /** * Namespace the storage key so multiple providers can share one storage * adapter without colliding (e.g. one session per account). The effective key * becomes `${keyPrefix}zyphr.session`. Default `''`. */ keyPrefix?: string; /** Refresh this many ms before the access token expires. Default 60_000 (from auth-core). */ refreshSkewMs?: number; children: ReactNode; } interface ZyphrAuthContextValue { manager: SessionManager; } const ZyphrAuthContext = createContext(null); /** * Provides a reactive Zyphr auth session to the React tree. Constructs a single * {@link SessionManager} (from `@zyphr-dev/auth-core`) driven by the given * ZyphrClient, initializes it (adopting any persisted session), and cleans up on * unmount. Read state with {@link useSession}; act with {@link useAuth}. */ export function ZyphrAuthProvider({ client, storage, keyPrefix, refreshSkewMs, children, }: ZyphrAuthProviderProps): ReactElement { // One manager for the lifetime of the provider. Recreated only if the client // identity changes (rare — normally a stable singleton). const [manager] = useState( () => new SessionManager({ transport: createZyphrTransport(client), storage: storage ?? createWebStorage(), keyPrefix, refreshSkewMs, }), ); const initialized = useRef(false); useEffect(() => { if (!initialized.current) { initialized.current = true; void manager.initialize(); } return () => { manager.destroy(); }; }, [manager]); const value = useMemo(() => ({ manager }), [manager]); return {children}; } function useManager(): SessionManager { const ctx = useContext(ZyphrAuthContext); if (!ctx) { throw new Error('useSession/useAuth must be used within a .'); } return ctx.manager; } /** * Reactive session state — re-renders on every session change (login, logout, * refresh). Returns `{ status, accessToken, isAuthenticated }`. */ export function useSession(): SessionState { const manager = useManager(); return useSyncExternalStore( (onChange) => manager.subscribe(() => onChange()), () => manager.getState(), () => manager.getState(), ); } export interface UseAuthResult { login: (params: LoginParams) => Promise; logout: () => Promise; refresh: () => Promise; } /** Auth actions bound to the provider's session manager. */ export function useAuth(): UseAuthResult { const manager = useManager(); return useMemo( () => ({ login: (params) => manager.login(params), logout: () => manager.logout(), refresh: () => manager.refresh(), }), [manager], ); }