@clarityops/preferences 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/PreferencesContext.tsx","../src/PreferencesService.ts","../src/avatar-cache.ts"],"sourcesContent":["/**\n * @clarityops/preferences\n *\n * Shared user preferences context for InsightForge and InsightFlow platforms.\n * Provides unified avatar, theme, and notification preferences management.\n */\n\n// Context and hooks\nexport {\n PreferencesProvider,\n usePreferences,\n usePreferencesSafe,\n useAvatar,\n type PreferencesProviderProps,\n} from './PreferencesContext';\n\n// Service\nexport { PreferencesService } from './PreferencesService';\n\n// Types\nexport type {\n UserPreferences,\n AvatarData,\n PreferencesConfig,\n PreferencesContextValue,\n PrepareAvatarUploadResponse,\n ConfirmAvatarUploadResponse,\n MyAvatarResponse,\n} from './types';\n\n// Cache utilities\nexport {\n getCachedAvatar,\n setCachedAvatar,\n clearAvatarCache,\n hasCachedAvatar,\n AVATAR_CACHE_TTL,\n} from './avatar-cache';\n","import React, {\n createContext,\n useContext,\n useState,\n useEffect,\n useCallback,\n useRef,\n type ReactNode,\n} from 'react';\nimport type {\n UserPreferences,\n AvatarData,\n PreferencesConfig,\n PreferencesContextValue,\n} from './types';\nimport { PreferencesService } from './PreferencesService';\nimport {\n getCachedAvatar,\n setCachedAvatar,\n clearAvatarCache as clearCache,\n} from './avatar-cache';\n\nconst PreferencesContext = createContext<PreferencesContextValue | null>(null);\n\nexport interface PreferencesProviderProps {\n children: ReactNode;\n config: PreferencesConfig;\n /** Optional initial preferences to avoid loading state */\n initialPreferences?: UserPreferences;\n}\n\nexport function PreferencesProvider({\n children,\n config,\n initialPreferences,\n}: PreferencesProviderProps) {\n const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || {});\n const [loading, setLoading] = useState(!initialPreferences);\n const [error, setError] = useState<string | null>(null);\n\n // Avatar state\n const [avatarUrl, setAvatarUrl] = useState<string | null>(null);\n const [hasAvatar, setHasAvatar] = useState(false);\n const [avatarLoading, setAvatarLoading] = useState(true);\n const [avatarError, setAvatarError] = useState<string | null>(null);\n const refreshAttemptRef = useRef(0);\n\n // Store config in a ref to avoid dependency issues\n const configRef = useRef(config);\n configRef.current = config;\n\n // Service instance\n const serviceRef = useRef<PreferencesService | null>(null);\n if (!serviceRef.current) {\n serviceRef.current = new PreferencesService(config);\n }\n \n // Update service config when getToken or onTokenInvalid changes\n useEffect(() => {\n if (serviceRef.current) {\n serviceRef.current.updateConfig({\n getToken: config.getToken,\n onTokenInvalid: config.onTokenInvalid,\n });\n }\n }, [config.getToken, config.onTokenInvalid]);\n\n // Fetch preferences\n const refreshPreferences = useCallback(async () => {\n setLoading(true);\n setError(null);\n\n try {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const response = await service.getPreferences();\n setPreferences(response.data.preferences);\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to load preferences';\n setError(msg);\n console.error('Preferences fetch error:', err);\n } finally {\n setLoading(false);\n }\n }, []);\n\n // Fetch avatar\n const fetchAvatar = useCallback(async (skipCache = false) => {\n const cacheKey = 'me';\n\n // Check cache first\n if (!skipCache) {\n const cached = getCachedAvatar(cacheKey);\n if (cached) {\n setAvatarUrl(cached);\n setHasAvatar(true);\n setAvatarLoading(false);\n return;\n }\n }\n\n setAvatarLoading(true);\n setAvatarError(null);\n\n try {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const data = await service.getMyAvatar();\n\n if (data.has_avatar && data.avatar_url) {\n setAvatarUrl(data.avatar_url);\n setHasAvatar(true);\n setCachedAvatar(cacheKey, data.avatar_url);\n refreshAttemptRef.current = 0;\n } else {\n setAvatarUrl(null);\n setHasAvatar(false);\n clearCache(cacheKey);\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to load avatar';\n setAvatarError(msg);\n setAvatarUrl(null);\n setHasAvatar(false);\n\n // Don't log 404s as errors\n if (!msg.includes('404') && !msg.includes('not found')) {\n console.error('Avatar fetch error:', err);\n }\n } finally {\n setAvatarLoading(false);\n }\n }, []);\n\n // Refresh avatar (bypasses cache)\n const refreshAvatar = useCallback(async () => {\n await fetchAvatar(true);\n }, [fetchAvatar]);\n\n // Handle image error (403/expired)\n const handleImageError = useCallback(() => {\n if (refreshAttemptRef.current >= 2) {\n console.warn('Avatar refresh limit reached');\n setAvatarUrl(null);\n setHasAvatar(false);\n return;\n }\n\n refreshAttemptRef.current += 1;\n console.log('Avatar image failed, refreshing (attempt', refreshAttemptRef.current, ')');\n\n clearCache('me');\n fetchAvatar(true);\n }, [fetchAvatar]);\n\n // Update preferences\n const updatePreferences = useCallback(async (prefs: Partial<UserPreferences>) => {\n try {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const response = await service.updatePreferences(prefs);\n setPreferences(response.data.preferences);\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to update preferences';\n setError(msg);\n throw err;\n }\n }, []);\n\n // Upload avatar\n const uploadAvatar = useCallback(async (file: File): Promise<string> => {\n const service = serviceRef.current;\n if (!service) throw new Error('Service not initialized');\n\n const url = await service.uploadAvatar(file);\n\n // Clear cache and refresh\n clearCache('me');\n await fetchAvatar(true);\n\n return url;\n }, [fetchAvatar]);\n\n // Clear avatar cache utility\n const clearAvatarCache = useCallback((userId?: string) => {\n clearCache(userId || 'me');\n }, []);\n\n // Apply theme to document\n const applyTheme = useCallback((theme: 'light' | 'dark' | 'system' | undefined) => {\n const root = document.documentElement;\n if (theme === 'dark') {\n root.classList.add('dark');\n } else if (theme === 'light') {\n root.classList.remove('dark');\n } else {\n // System preference\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n if (prefersDark) {\n root.classList.add('dark');\n } else {\n root.classList.remove('dark');\n }\n }\n }, []);\n\n // Sync preferences on login - fetches and applies theme immediately\n const syncOnLogin = useCallback(async () => {\n try {\n const service = serviceRef.current;\n if (!service) return;\n\n const response = await service.getPreferences();\n const prefs = response.data.preferences;\n setPreferences(prefs);\n \n // Apply theme immediately\n applyTheme(prefs.theme);\n \n // Also fetch avatar\n await fetchAvatar(true);\n } catch (err) {\n console.error('Failed to sync preferences on login:', err);\n }\n }, [applyTheme, fetchAvatar]);\n\n // Initial fetch - only if we have a token (run once on mount)\n useEffect(() => {\n const hasToken = !!configRef.current.getToken();\n if (!hasToken) {\n // No token yet, skip initial fetch - will be triggered by syncOnLogin\n setLoading(false);\n setAvatarLoading(false);\n return;\n }\n \n if (!initialPreferences) {\n refreshPreferences();\n }\n fetchAvatar();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []); // Run only on mount - config is accessed via ref\n\n // Apply theme when preferences change (only if we have a token)\n useEffect(() => {\n const hasToken = !!configRef.current.getToken();\n if (hasToken && preferences.theme) {\n applyTheme(preferences.theme);\n }\n }, [preferences.theme, applyTheme]);\n\n const avatar: AvatarData = {\n avatarUrl,\n hasAvatar,\n loading: avatarLoading,\n error: avatarError,\n refresh: refreshAvatar,\n handleImageError,\n };\n\n const value: PreferencesContextValue = {\n preferences,\n loading,\n error,\n avatar,\n updatePreferences,\n refreshPreferences,\n uploadAvatar,\n clearAvatarCache,\n syncOnLogin,\n };\n\n return (\n <PreferencesContext.Provider value={value}>\n {children}\n </PreferencesContext.Provider>\n );\n}\n\n/**\n * Hook to access preferences context\n * Throws if used outside PreferencesProvider\n */\nexport function usePreferences(): PreferencesContextValue {\n const context = useContext(PreferencesContext);\n if (!context) {\n throw new Error('usePreferences must be used within a PreferencesProvider');\n }\n return context;\n}\n\n/**\n * Hook to safely access preferences context\n * Returns null if used outside PreferencesProvider (useful during hot reload)\n */\nexport function usePreferencesSafe(): PreferencesContextValue | null {\n return useContext(PreferencesContext);\n}\n\n/**\n * Hook to access just avatar data\n */\nexport function useAvatar(): AvatarData {\n const { avatar } = usePreferences();\n return avatar;\n}\n","import type {\n UserPreferences,\n PrepareAvatarUploadResponse,\n ConfirmAvatarUploadResponse,\n MyAvatarResponse,\n PreferencesConfig,\n} from './types';\n\n/**\n * Validate if a token is a well-formed JWT with 3 segments\n */\nfunction isValidJwtFormat(token: string | null | undefined): boolean {\n if (!token || typeof token !== 'string') return false;\n // Check for placeholder values\n if (token === 'null' || token === 'undefined' || token.trim() === '') return false;\n // JWT must have exactly 3 segments\n const segments = token.split('.');\n return segments.length === 3;\n}\n\n/**\n * Check if a JWT token is expired\n */\nfunction isTokenExpired(token: string): boolean {\n try {\n const payload = JSON.parse(atob(token.split('.')[1]));\n const currentTime = Math.floor(Date.now() / 1000);\n return payload.exp < currentTime;\n } catch {\n return true;\n }\n}\n\n// Shared refresh lock to prevent concurrent refresh attempts across service instances\nlet refreshInProgress: Promise<string | null> | null = null;\nlet lastRefreshTime = 0;\nconst REFRESH_DEBOUNCE_MS = 5000; // Minimum 5 seconds between refresh attempts\n\n/**\n * Service class for user preferences and avatar management\n * Shared across InsightForge and InsightFlow platforms\n */\nexport class PreferencesService {\n private baseUrl: string;\n private brandingUrl: string;\n private getToken: () => string | null;\n private onTokenInvalid?: () => Promise<string | null>;\n\n constructor(config: PreferencesConfig) {\n this.baseUrl = config.apiBaseUrl;\n this.brandingUrl = config.brandingUrl;\n this.getToken = config.getToken;\n this.onTokenInvalid = config.onTokenInvalid;\n }\n\n /**\n * Update the config (e.g., when token changes)\n */\n updateConfig(config: Partial<PreferencesConfig>) {\n if (config.getToken) {\n this.getToken = config.getToken;\n }\n if (config.onTokenInvalid !== undefined) {\n this.onTokenInvalid = config.onTokenInvalid;\n }\n if (config.apiBaseUrl) {\n this.baseUrl = config.apiBaseUrl;\n }\n if (config.brandingUrl) {\n this.brandingUrl = config.brandingUrl;\n }\n }\n\n /**\n * Refresh token with debounce to prevent multiple simultaneous refresh attempts\n */\n private async refreshWithDebounce(): Promise<string | null> {\n const now = Date.now();\n \n // If a refresh is already in progress, wait for it\n if (refreshInProgress) {\n console.log('[PreferencesService] ⏳ Waiting for existing refresh to complete...');\n return refreshInProgress;\n }\n \n // Debounce: if we just refreshed, get current token from localStorage\n if (now - lastRefreshTime < REFRESH_DEBOUNCE_MS) {\n console.log('[PreferencesService] ⏳ Recent refresh detected, using current token');\n return this.getToken();\n }\n \n // Start a new refresh\n if (this.onTokenInvalid) {\n console.log('[PreferencesService] 🔄 Starting token refresh...');\n lastRefreshTime = now;\n refreshInProgress = this.onTokenInvalid().finally(() => {\n refreshInProgress = null;\n });\n return refreshInProgress;\n }\n \n return null;\n }\n\n /**\n * Get a valid token, refreshing if necessary with debounce protection\n */\n private async getValidToken(): Promise<string> {\n let token = this.getToken();\n \n // Check if token is valid format\n if (!isValidJwtFormat(token)) {\n console.warn('[PreferencesService] Token is missing or malformed, attempting refresh...');\n token = await this.refreshWithDebounce();\n if (!isValidJwtFormat(token)) {\n throw new Error('Authentication required - no valid token available');\n }\n }\n \n // Check if token is expired\n if (isTokenExpired(token!)) {\n console.warn('[PreferencesService] Token is expired, attempting refresh...');\n token = await this.refreshWithDebounce();\n if (!token || isTokenExpired(token)) {\n throw new Error('Session expired - please log in again');\n }\n }\n \n return token!;\n }\n\n private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.baseUrl}${endpoint}`, {\n ...options,\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Request failed' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401, attempting token refresh and retry...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.baseUrl}${endpoint}`, {\n ...options,\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n 'Content-Type': 'application/json',\n ...options.headers,\n },\n });\n \n if (retryResponse.ok) {\n return retryResponse.json();\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Request failed');\n }\n\n const data = await response.json();\n return data;\n }\n\n /**\n * Get user preferences\n */\n async getPreferences(): Promise<{ data: { preferences: UserPreferences } }> {\n return this.request('/forge/users/me/preferences');\n }\n\n /**\n * Update preferences (merge update)\n */\n async updatePreferences(preferences: Partial<UserPreferences>): Promise<{ data: { preferences: UserPreferences }; message?: string }> {\n return this.request('/forge/users/me/preferences', {\n method: 'PATCH',\n body: JSON.stringify(preferences),\n });\n }\n\n /**\n * Replace all preferences\n */\n async replacePreferences(preferences: UserPreferences): Promise<{ data: { preferences: UserPreferences }; message?: string }> {\n return this.request('/forge/users/me/preferences', {\n method: 'PUT',\n body: JSON.stringify(preferences),\n });\n }\n\n /**\n * Step 1: Prepare avatar upload - get signed URL for GCS\n */\n async prepareAvatarUpload(filename: string, mimeType: string): Promise<PrepareAvatarUploadResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n filename,\n mime_type: mimeType,\n }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to prepare upload' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on prepareAvatarUpload, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n filename,\n mime_type: mimeType,\n }),\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to prepare avatar upload');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to prepare avatar upload');\n }\n\n return data.data;\n }\n\n /**\n * Step 2: Upload file directly to GCS\n */\n async uploadToGCS(uploadUrl: string, file: File): Promise<void> {\n const response = await fetch(uploadUrl, {\n method: 'PUT',\n headers: {\n 'Content-Type': file.type,\n },\n body: file,\n });\n\n if (!response.ok) {\n throw new Error('Failed to upload file to storage');\n }\n }\n\n /**\n * Step 3: Confirm avatar upload\n */\n async confirmAvatarUpload(storagePath: string): Promise<ConfirmAvatarUploadResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n storage_path: storagePath,\n }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to confirm upload' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on confirmAvatarUpload, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n storage_path: storagePath,\n }),\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to confirm avatar upload');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to confirm avatar upload');\n }\n\n return data.data;\n }\n\n /**\n * Get current user's avatar\n */\n async getMyAvatar(): Promise<MyAvatarResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/my-avatar`, {\n headers: {\n 'Authorization': `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to fetch avatar' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on getMyAvatar, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/my-avatar`, {\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n },\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to fetch avatar');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to fetch avatar');\n }\n\n return data.data;\n }\n\n /**\n * Get any user's avatar\n */\n async getUserAvatar(userId: string): Promise<MyAvatarResponse> {\n const token = await this.getValidToken();\n \n const response = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {\n headers: {\n 'Authorization': `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Failed to fetch avatar' }));\n \n // If we get a 401, try to refresh and retry once\n if (response.status === 401 && this.onTokenInvalid) {\n console.warn('[PreferencesService] Got 401 on getUserAvatar, attempting refresh...');\n const newToken = await this.onTokenInvalid();\n if (newToken && isValidJwtFormat(newToken)) {\n const retryResponse = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {\n headers: {\n 'Authorization': `Bearer ${newToken}`,\n },\n });\n \n if (retryResponse.ok) {\n const data = await retryResponse.json();\n if (data.success) return data.data;\n }\n }\n }\n \n throw new Error(error.error || error.message || 'Failed to fetch avatar');\n }\n\n const data = await response.json();\n if (!data.success) {\n throw new Error(data.error || 'Failed to fetch avatar');\n }\n\n return data.data;\n }\n\n /**\n * Complete avatar upload flow (3-step process)\n */\n async uploadAvatar(file: File): Promise<string> {\n // Validate file type\n const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'];\n if (!validTypes.includes(file.type)) {\n throw new Error('Please select a PNG, JPEG, or WebP image');\n }\n\n // Validate file size (5MB max)\n const maxSize = 5 * 1024 * 1024;\n if (file.size > maxSize) {\n throw new Error('Image must be less than 5MB');\n }\n\n // Step 1: Prepare upload\n const prepareData = await this.prepareAvatarUpload(file.name, file.type);\n\n // Step 2: Upload to GCS\n await this.uploadToGCS(prepareData.upload_url, file);\n\n // Step 3: Confirm upload\n const confirmData = await this.confirmAvatarUpload(prepareData.storage_path);\n\n return confirmData.avatar_url;\n }\n}\n","/**\n * In-memory cache for avatar URLs\n * Shared across all components using the preferences context\n */\n\ninterface CacheEntry {\n url: string;\n cachedAt: number;\n}\n\n// Cache TTL: 1 day (as per API guidance)\nexport const AVATAR_CACHE_TTL = 24 * 60 * 60 * 1000;\n\n// Global in-memory cache\nconst avatarCache = new Map<string, CacheEntry>();\n\n/**\n * Get cached avatar URL if valid\n */\nexport function getCachedAvatar(userId: string): string | null {\n const cached = avatarCache.get(userId);\n if (cached && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL) {\n return cached.url;\n }\n return null;\n}\n\n/**\n * Set avatar URL in cache\n */\nexport function setCachedAvatar(userId: string, url: string): void {\n avatarCache.set(userId, {\n url,\n cachedAt: Date.now(),\n });\n}\n\n/**\n * Clear cached avatar for a specific user or all users\n */\nexport function clearAvatarCache(userId?: string): void {\n if (userId) {\n avatarCache.delete(userId);\n } else {\n avatarCache.clear();\n }\n}\n\n/**\n * Check if cache entry exists and is valid\n */\nexport function hasCachedAvatar(userId: string): boolean {\n const cached = avatarCache.get(userId);\n return cached !== undefined && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL;\n}\n"],"mappings":"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","names":["data","clearAvatarCache"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,596 @@
1
+ // src/PreferencesContext.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useEffect,
7
+ useCallback,
8
+ useRef
9
+ } from "react";
10
+
11
+ // src/PreferencesService.ts
12
+ function isValidJwtFormat(token) {
13
+ if (!token || typeof token !== "string") return false;
14
+ if (token === "null" || token === "undefined" || token.trim() === "") return false;
15
+ const segments = token.split(".");
16
+ return segments.length === 3;
17
+ }
18
+ function isTokenExpired(token) {
19
+ try {
20
+ const payload = JSON.parse(atob(token.split(".")[1]));
21
+ const currentTime = Math.floor(Date.now() / 1e3);
22
+ return payload.exp < currentTime;
23
+ } catch {
24
+ return true;
25
+ }
26
+ }
27
+ var refreshInProgress = null;
28
+ var lastRefreshTime = 0;
29
+ var REFRESH_DEBOUNCE_MS = 5e3;
30
+ var PreferencesService = class {
31
+ constructor(config) {
32
+ this.baseUrl = config.apiBaseUrl;
33
+ this.brandingUrl = config.brandingUrl;
34
+ this.getToken = config.getToken;
35
+ this.onTokenInvalid = config.onTokenInvalid;
36
+ }
37
+ /**
38
+ * Update the config (e.g., when token changes)
39
+ */
40
+ updateConfig(config) {
41
+ if (config.getToken) {
42
+ this.getToken = config.getToken;
43
+ }
44
+ if (config.onTokenInvalid !== void 0) {
45
+ this.onTokenInvalid = config.onTokenInvalid;
46
+ }
47
+ if (config.apiBaseUrl) {
48
+ this.baseUrl = config.apiBaseUrl;
49
+ }
50
+ if (config.brandingUrl) {
51
+ this.brandingUrl = config.brandingUrl;
52
+ }
53
+ }
54
+ /**
55
+ * Refresh token with debounce to prevent multiple simultaneous refresh attempts
56
+ */
57
+ async refreshWithDebounce() {
58
+ const now = Date.now();
59
+ if (refreshInProgress) {
60
+ console.log("[PreferencesService] \u23F3 Waiting for existing refresh to complete...");
61
+ return refreshInProgress;
62
+ }
63
+ if (now - lastRefreshTime < REFRESH_DEBOUNCE_MS) {
64
+ console.log("[PreferencesService] \u23F3 Recent refresh detected, using current token");
65
+ return this.getToken();
66
+ }
67
+ if (this.onTokenInvalid) {
68
+ console.log("[PreferencesService] \u{1F504} Starting token refresh...");
69
+ lastRefreshTime = now;
70
+ refreshInProgress = this.onTokenInvalid().finally(() => {
71
+ refreshInProgress = null;
72
+ });
73
+ return refreshInProgress;
74
+ }
75
+ return null;
76
+ }
77
+ /**
78
+ * Get a valid token, refreshing if necessary with debounce protection
79
+ */
80
+ async getValidToken() {
81
+ let token = this.getToken();
82
+ if (!isValidJwtFormat(token)) {
83
+ console.warn("[PreferencesService] Token is missing or malformed, attempting refresh...");
84
+ token = await this.refreshWithDebounce();
85
+ if (!isValidJwtFormat(token)) {
86
+ throw new Error("Authentication required - no valid token available");
87
+ }
88
+ }
89
+ if (isTokenExpired(token)) {
90
+ console.warn("[PreferencesService] Token is expired, attempting refresh...");
91
+ token = await this.refreshWithDebounce();
92
+ if (!token || isTokenExpired(token)) {
93
+ throw new Error("Session expired - please log in again");
94
+ }
95
+ }
96
+ return token;
97
+ }
98
+ async request(endpoint, options = {}) {
99
+ const token = await this.getValidToken();
100
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
101
+ ...options,
102
+ headers: {
103
+ "Authorization": `Bearer ${token}`,
104
+ "Content-Type": "application/json",
105
+ ...options.headers
106
+ }
107
+ });
108
+ if (!response.ok) {
109
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
110
+ if (response.status === 401 && this.onTokenInvalid) {
111
+ console.warn("[PreferencesService] Got 401, attempting token refresh and retry...");
112
+ const newToken = await this.onTokenInvalid();
113
+ if (newToken && isValidJwtFormat(newToken)) {
114
+ const retryResponse = await fetch(`${this.baseUrl}${endpoint}`, {
115
+ ...options,
116
+ headers: {
117
+ "Authorization": `Bearer ${newToken}`,
118
+ "Content-Type": "application/json",
119
+ ...options.headers
120
+ }
121
+ });
122
+ if (retryResponse.ok) {
123
+ return retryResponse.json();
124
+ }
125
+ }
126
+ }
127
+ throw new Error(error.error || error.message || "Request failed");
128
+ }
129
+ const data = await response.json();
130
+ return data;
131
+ }
132
+ /**
133
+ * Get user preferences
134
+ */
135
+ async getPreferences() {
136
+ return this.request("/forge/users/me/preferences");
137
+ }
138
+ /**
139
+ * Update preferences (merge update)
140
+ */
141
+ async updatePreferences(preferences) {
142
+ return this.request("/forge/users/me/preferences", {
143
+ method: "PATCH",
144
+ body: JSON.stringify(preferences)
145
+ });
146
+ }
147
+ /**
148
+ * Replace all preferences
149
+ */
150
+ async replacePreferences(preferences) {
151
+ return this.request("/forge/users/me/preferences", {
152
+ method: "PUT",
153
+ body: JSON.stringify(preferences)
154
+ });
155
+ }
156
+ /**
157
+ * Step 1: Prepare avatar upload - get signed URL for GCS
158
+ */
159
+ async prepareAvatarUpload(filename, mimeType) {
160
+ const token = await this.getValidToken();
161
+ const response = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {
162
+ method: "POST",
163
+ headers: {
164
+ "Authorization": `Bearer ${token}`,
165
+ "Content-Type": "application/json"
166
+ },
167
+ body: JSON.stringify({
168
+ filename,
169
+ mime_type: mimeType
170
+ })
171
+ });
172
+ if (!response.ok) {
173
+ const error = await response.json().catch(() => ({ error: "Failed to prepare upload" }));
174
+ if (response.status === 401 && this.onTokenInvalid) {
175
+ console.warn("[PreferencesService] Got 401 on prepareAvatarUpload, attempting refresh...");
176
+ const newToken = await this.onTokenInvalid();
177
+ if (newToken && isValidJwtFormat(newToken)) {
178
+ const retryResponse = await fetch(`${this.brandingUrl}/branding/prepare-avatar-upload`, {
179
+ method: "POST",
180
+ headers: {
181
+ "Authorization": `Bearer ${newToken}`,
182
+ "Content-Type": "application/json"
183
+ },
184
+ body: JSON.stringify({
185
+ filename,
186
+ mime_type: mimeType
187
+ })
188
+ });
189
+ if (retryResponse.ok) {
190
+ const data2 = await retryResponse.json();
191
+ if (data2.success) return data2.data;
192
+ }
193
+ }
194
+ }
195
+ throw new Error(error.error || error.message || "Failed to prepare avatar upload");
196
+ }
197
+ const data = await response.json();
198
+ if (!data.success) {
199
+ throw new Error(data.error || "Failed to prepare avatar upload");
200
+ }
201
+ return data.data;
202
+ }
203
+ /**
204
+ * Step 2: Upload file directly to GCS
205
+ */
206
+ async uploadToGCS(uploadUrl, file) {
207
+ const response = await fetch(uploadUrl, {
208
+ method: "PUT",
209
+ headers: {
210
+ "Content-Type": file.type
211
+ },
212
+ body: file
213
+ });
214
+ if (!response.ok) {
215
+ throw new Error("Failed to upload file to storage");
216
+ }
217
+ }
218
+ /**
219
+ * Step 3: Confirm avatar upload
220
+ */
221
+ async confirmAvatarUpload(storagePath) {
222
+ const token = await this.getValidToken();
223
+ const response = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {
224
+ method: "POST",
225
+ headers: {
226
+ "Authorization": `Bearer ${token}`,
227
+ "Content-Type": "application/json"
228
+ },
229
+ body: JSON.stringify({
230
+ storage_path: storagePath
231
+ })
232
+ });
233
+ if (!response.ok) {
234
+ const error = await response.json().catch(() => ({ error: "Failed to confirm upload" }));
235
+ if (response.status === 401 && this.onTokenInvalid) {
236
+ console.warn("[PreferencesService] Got 401 on confirmAvatarUpload, attempting refresh...");
237
+ const newToken = await this.onTokenInvalid();
238
+ if (newToken && isValidJwtFormat(newToken)) {
239
+ const retryResponse = await fetch(`${this.brandingUrl}/branding/confirm-avatar-upload`, {
240
+ method: "POST",
241
+ headers: {
242
+ "Authorization": `Bearer ${newToken}`,
243
+ "Content-Type": "application/json"
244
+ },
245
+ body: JSON.stringify({
246
+ storage_path: storagePath
247
+ })
248
+ });
249
+ if (retryResponse.ok) {
250
+ const data2 = await retryResponse.json();
251
+ if (data2.success) return data2.data;
252
+ }
253
+ }
254
+ }
255
+ throw new Error(error.error || error.message || "Failed to confirm avatar upload");
256
+ }
257
+ const data = await response.json();
258
+ if (!data.success) {
259
+ throw new Error(data.error || "Failed to confirm avatar upload");
260
+ }
261
+ return data.data;
262
+ }
263
+ /**
264
+ * Get current user's avatar
265
+ */
266
+ async getMyAvatar() {
267
+ const token = await this.getValidToken();
268
+ const response = await fetch(`${this.brandingUrl}/branding/my-avatar`, {
269
+ headers: {
270
+ "Authorization": `Bearer ${token}`
271
+ }
272
+ });
273
+ if (!response.ok) {
274
+ const error = await response.json().catch(() => ({ error: "Failed to fetch avatar" }));
275
+ if (response.status === 401 && this.onTokenInvalid) {
276
+ console.warn("[PreferencesService] Got 401 on getMyAvatar, attempting refresh...");
277
+ const newToken = await this.onTokenInvalid();
278
+ if (newToken && isValidJwtFormat(newToken)) {
279
+ const retryResponse = await fetch(`${this.brandingUrl}/branding/my-avatar`, {
280
+ headers: {
281
+ "Authorization": `Bearer ${newToken}`
282
+ }
283
+ });
284
+ if (retryResponse.ok) {
285
+ const data2 = await retryResponse.json();
286
+ if (data2.success) return data2.data;
287
+ }
288
+ }
289
+ }
290
+ throw new Error(error.error || error.message || "Failed to fetch avatar");
291
+ }
292
+ const data = await response.json();
293
+ if (!data.success) {
294
+ throw new Error(data.error || "Failed to fetch avatar");
295
+ }
296
+ return data.data;
297
+ }
298
+ /**
299
+ * Get any user's avatar
300
+ */
301
+ async getUserAvatar(userId) {
302
+ const token = await this.getValidToken();
303
+ const response = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {
304
+ headers: {
305
+ "Authorization": `Bearer ${token}`
306
+ }
307
+ });
308
+ if (!response.ok) {
309
+ const error = await response.json().catch(() => ({ error: "Failed to fetch avatar" }));
310
+ if (response.status === 401 && this.onTokenInvalid) {
311
+ console.warn("[PreferencesService] Got 401 on getUserAvatar, attempting refresh...");
312
+ const newToken = await this.onTokenInvalid();
313
+ if (newToken && isValidJwtFormat(newToken)) {
314
+ const retryResponse = await fetch(`${this.brandingUrl}/branding/users/${userId}/avatar`, {
315
+ headers: {
316
+ "Authorization": `Bearer ${newToken}`
317
+ }
318
+ });
319
+ if (retryResponse.ok) {
320
+ const data2 = await retryResponse.json();
321
+ if (data2.success) return data2.data;
322
+ }
323
+ }
324
+ }
325
+ throw new Error(error.error || error.message || "Failed to fetch avatar");
326
+ }
327
+ const data = await response.json();
328
+ if (!data.success) {
329
+ throw new Error(data.error || "Failed to fetch avatar");
330
+ }
331
+ return data.data;
332
+ }
333
+ /**
334
+ * Complete avatar upload flow (3-step process)
335
+ */
336
+ async uploadAvatar(file) {
337
+ const validTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
338
+ if (!validTypes.includes(file.type)) {
339
+ throw new Error("Please select a PNG, JPEG, or WebP image");
340
+ }
341
+ const maxSize = 5 * 1024 * 1024;
342
+ if (file.size > maxSize) {
343
+ throw new Error("Image must be less than 5MB");
344
+ }
345
+ const prepareData = await this.prepareAvatarUpload(file.name, file.type);
346
+ await this.uploadToGCS(prepareData.upload_url, file);
347
+ const confirmData = await this.confirmAvatarUpload(prepareData.storage_path);
348
+ return confirmData.avatar_url;
349
+ }
350
+ };
351
+
352
+ // src/avatar-cache.ts
353
+ var AVATAR_CACHE_TTL = 24 * 60 * 60 * 1e3;
354
+ var avatarCache = /* @__PURE__ */ new Map();
355
+ function getCachedAvatar(userId) {
356
+ const cached = avatarCache.get(userId);
357
+ if (cached && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL) {
358
+ return cached.url;
359
+ }
360
+ return null;
361
+ }
362
+ function setCachedAvatar(userId, url) {
363
+ avatarCache.set(userId, {
364
+ url,
365
+ cachedAt: Date.now()
366
+ });
367
+ }
368
+ function clearAvatarCache(userId) {
369
+ if (userId) {
370
+ avatarCache.delete(userId);
371
+ } else {
372
+ avatarCache.clear();
373
+ }
374
+ }
375
+ function hasCachedAvatar(userId) {
376
+ const cached = avatarCache.get(userId);
377
+ return cached !== void 0 && Date.now() - cached.cachedAt < AVATAR_CACHE_TTL;
378
+ }
379
+
380
+ // src/PreferencesContext.tsx
381
+ import { jsx } from "react/jsx-runtime";
382
+ var PreferencesContext = createContext(null);
383
+ function PreferencesProvider({
384
+ children,
385
+ config,
386
+ initialPreferences
387
+ }) {
388
+ const [preferences, setPreferences] = useState(initialPreferences || {});
389
+ const [loading, setLoading] = useState(!initialPreferences);
390
+ const [error, setError] = useState(null);
391
+ const [avatarUrl, setAvatarUrl] = useState(null);
392
+ const [hasAvatar, setHasAvatar] = useState(false);
393
+ const [avatarLoading, setAvatarLoading] = useState(true);
394
+ const [avatarError, setAvatarError] = useState(null);
395
+ const refreshAttemptRef = useRef(0);
396
+ const configRef = useRef(config);
397
+ configRef.current = config;
398
+ const serviceRef = useRef(null);
399
+ if (!serviceRef.current) {
400
+ serviceRef.current = new PreferencesService(config);
401
+ }
402
+ useEffect(() => {
403
+ if (serviceRef.current) {
404
+ serviceRef.current.updateConfig({
405
+ getToken: config.getToken,
406
+ onTokenInvalid: config.onTokenInvalid
407
+ });
408
+ }
409
+ }, [config.getToken, config.onTokenInvalid]);
410
+ const refreshPreferences = useCallback(async () => {
411
+ setLoading(true);
412
+ setError(null);
413
+ try {
414
+ const service = serviceRef.current;
415
+ if (!service) throw new Error("Service not initialized");
416
+ const response = await service.getPreferences();
417
+ setPreferences(response.data.preferences);
418
+ } catch (err) {
419
+ const msg = err instanceof Error ? err.message : "Failed to load preferences";
420
+ setError(msg);
421
+ console.error("Preferences fetch error:", err);
422
+ } finally {
423
+ setLoading(false);
424
+ }
425
+ }, []);
426
+ const fetchAvatar = useCallback(async (skipCache = false) => {
427
+ const cacheKey = "me";
428
+ if (!skipCache) {
429
+ const cached = getCachedAvatar(cacheKey);
430
+ if (cached) {
431
+ setAvatarUrl(cached);
432
+ setHasAvatar(true);
433
+ setAvatarLoading(false);
434
+ return;
435
+ }
436
+ }
437
+ setAvatarLoading(true);
438
+ setAvatarError(null);
439
+ try {
440
+ const service = serviceRef.current;
441
+ if (!service) throw new Error("Service not initialized");
442
+ const data = await service.getMyAvatar();
443
+ if (data.has_avatar && data.avatar_url) {
444
+ setAvatarUrl(data.avatar_url);
445
+ setHasAvatar(true);
446
+ setCachedAvatar(cacheKey, data.avatar_url);
447
+ refreshAttemptRef.current = 0;
448
+ } else {
449
+ setAvatarUrl(null);
450
+ setHasAvatar(false);
451
+ clearAvatarCache(cacheKey);
452
+ }
453
+ } catch (err) {
454
+ const msg = err instanceof Error ? err.message : "Failed to load avatar";
455
+ setAvatarError(msg);
456
+ setAvatarUrl(null);
457
+ setHasAvatar(false);
458
+ if (!msg.includes("404") && !msg.includes("not found")) {
459
+ console.error("Avatar fetch error:", err);
460
+ }
461
+ } finally {
462
+ setAvatarLoading(false);
463
+ }
464
+ }, []);
465
+ const refreshAvatar = useCallback(async () => {
466
+ await fetchAvatar(true);
467
+ }, [fetchAvatar]);
468
+ const handleImageError = useCallback(() => {
469
+ if (refreshAttemptRef.current >= 2) {
470
+ console.warn("Avatar refresh limit reached");
471
+ setAvatarUrl(null);
472
+ setHasAvatar(false);
473
+ return;
474
+ }
475
+ refreshAttemptRef.current += 1;
476
+ console.log("Avatar image failed, refreshing (attempt", refreshAttemptRef.current, ")");
477
+ clearAvatarCache("me");
478
+ fetchAvatar(true);
479
+ }, [fetchAvatar]);
480
+ const updatePreferences = useCallback(async (prefs) => {
481
+ try {
482
+ const service = serviceRef.current;
483
+ if (!service) throw new Error("Service not initialized");
484
+ const response = await service.updatePreferences(prefs);
485
+ setPreferences(response.data.preferences);
486
+ } catch (err) {
487
+ const msg = err instanceof Error ? err.message : "Failed to update preferences";
488
+ setError(msg);
489
+ throw err;
490
+ }
491
+ }, []);
492
+ const uploadAvatar = useCallback(async (file) => {
493
+ const service = serviceRef.current;
494
+ if (!service) throw new Error("Service not initialized");
495
+ const url = await service.uploadAvatar(file);
496
+ clearAvatarCache("me");
497
+ await fetchAvatar(true);
498
+ return url;
499
+ }, [fetchAvatar]);
500
+ const clearAvatarCache2 = useCallback((userId) => {
501
+ clearAvatarCache(userId || "me");
502
+ }, []);
503
+ const applyTheme = useCallback((theme) => {
504
+ const root = document.documentElement;
505
+ if (theme === "dark") {
506
+ root.classList.add("dark");
507
+ } else if (theme === "light") {
508
+ root.classList.remove("dark");
509
+ } else {
510
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
511
+ if (prefersDark) {
512
+ root.classList.add("dark");
513
+ } else {
514
+ root.classList.remove("dark");
515
+ }
516
+ }
517
+ }, []);
518
+ const syncOnLogin = useCallback(async () => {
519
+ try {
520
+ const service = serviceRef.current;
521
+ if (!service) return;
522
+ const response = await service.getPreferences();
523
+ const prefs = response.data.preferences;
524
+ setPreferences(prefs);
525
+ applyTheme(prefs.theme);
526
+ await fetchAvatar(true);
527
+ } catch (err) {
528
+ console.error("Failed to sync preferences on login:", err);
529
+ }
530
+ }, [applyTheme, fetchAvatar]);
531
+ useEffect(() => {
532
+ const hasToken = !!configRef.current.getToken();
533
+ if (!hasToken) {
534
+ setLoading(false);
535
+ setAvatarLoading(false);
536
+ return;
537
+ }
538
+ if (!initialPreferences) {
539
+ refreshPreferences();
540
+ }
541
+ fetchAvatar();
542
+ }, []);
543
+ useEffect(() => {
544
+ const hasToken = !!configRef.current.getToken();
545
+ if (hasToken && preferences.theme) {
546
+ applyTheme(preferences.theme);
547
+ }
548
+ }, [preferences.theme, applyTheme]);
549
+ const avatar = {
550
+ avatarUrl,
551
+ hasAvatar,
552
+ loading: avatarLoading,
553
+ error: avatarError,
554
+ refresh: refreshAvatar,
555
+ handleImageError
556
+ };
557
+ const value = {
558
+ preferences,
559
+ loading,
560
+ error,
561
+ avatar,
562
+ updatePreferences,
563
+ refreshPreferences,
564
+ uploadAvatar,
565
+ clearAvatarCache: clearAvatarCache2,
566
+ syncOnLogin
567
+ };
568
+ return /* @__PURE__ */ jsx(PreferencesContext.Provider, { value, children });
569
+ }
570
+ function usePreferences() {
571
+ const context = useContext(PreferencesContext);
572
+ if (!context) {
573
+ throw new Error("usePreferences must be used within a PreferencesProvider");
574
+ }
575
+ return context;
576
+ }
577
+ function usePreferencesSafe() {
578
+ return useContext(PreferencesContext);
579
+ }
580
+ function useAvatar() {
581
+ const { avatar } = usePreferences();
582
+ return avatar;
583
+ }
584
+ export {
585
+ AVATAR_CACHE_TTL,
586
+ PreferencesProvider,
587
+ PreferencesService,
588
+ clearAvatarCache,
589
+ getCachedAvatar,
590
+ hasCachedAvatar,
591
+ setCachedAvatar,
592
+ useAvatar,
593
+ usePreferences,
594
+ usePreferencesSafe
595
+ };
596
+ //# sourceMappingURL=index.mjs.map