style: auto-format codebase with Prettier

Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
This commit is contained in:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -6,76 +6,76 @@ let deviceAdapter: DeviceManagerAdapter | null = null;
* Set the device manager adapter for the auth service
*/
export function setDeviceAdapter(adapter: DeviceManagerAdapter): void {
deviceAdapter = adapter;
deviceAdapter = adapter;
}
/**
* Get the current device adapter
*/
export function getDeviceAdapter(): DeviceManagerAdapter {
if (!deviceAdapter) {
throw new Error(
'Device adapter not initialized. Call setDeviceAdapter() before using auth services.'
);
}
return deviceAdapter;
if (!deviceAdapter) {
throw new Error(
'Device adapter not initialized. Call setDeviceAdapter() before using auth services.'
);
}
return deviceAdapter;
}
/**
* Check if device adapter is initialized
*/
export function isDeviceInitialized(): boolean {
return deviceAdapter !== null;
return deviceAdapter !== null;
}
/**
* Create a web-based device manager adapter
*/
export function createWebDeviceAdapter(): DeviceManagerAdapter {
// Generate a persistent device ID for web
const getOrCreateDeviceId = (): string => {
const storageKey = '@manacore/deviceId';
let deviceId = localStorage.getItem(storageKey);
if (!deviceId) {
deviceId = crypto.randomUUID();
localStorage.setItem(storageKey, deviceId);
}
return deviceId;
};
// Generate a persistent device ID for web
const getOrCreateDeviceId = (): string => {
const storageKey = '@manacore/deviceId';
let deviceId = localStorage.getItem(storageKey);
if (!deviceId) {
deviceId = crypto.randomUUID();
localStorage.setItem(storageKey, deviceId);
}
return deviceId;
};
return {
async getDeviceInfo(): Promise<DeviceInfo> {
const userAgent = navigator.userAgent;
let deviceName = 'Web Browser';
let deviceType = 'web';
return {
async getDeviceInfo(): Promise<DeviceInfo> {
const userAgent = navigator.userAgent;
let deviceName = 'Web Browser';
let deviceType = 'web';
// Try to extract browser name
if (userAgent.includes('Chrome')) {
deviceName = 'Chrome Browser';
} else if (userAgent.includes('Safari')) {
deviceName = 'Safari Browser';
} else if (userAgent.includes('Firefox')) {
deviceName = 'Firefox Browser';
} else if (userAgent.includes('Edge')) {
deviceName = 'Edge Browser';
}
// Try to extract browser name
if (userAgent.includes('Chrome')) {
deviceName = 'Chrome Browser';
} else if (userAgent.includes('Safari')) {
deviceName = 'Safari Browser';
} else if (userAgent.includes('Firefox')) {
deviceName = 'Firefox Browser';
} else if (userAgent.includes('Edge')) {
deviceName = 'Edge Browser';
}
// Detect device type
if (/Mobi|Android/i.test(userAgent)) {
deviceType = 'mobile_web';
} else if (/Tablet|iPad/i.test(userAgent)) {
deviceType = 'tablet_web';
}
// Detect device type
if (/Mobi|Android/i.test(userAgent)) {
deviceType = 'mobile_web';
} else if (/Tablet|iPad/i.test(userAgent)) {
deviceType = 'tablet_web';
}
return {
deviceId: getOrCreateDeviceId(),
deviceName,
deviceType,
platform: 'web',
};
},
async getStoredDeviceId(): Promise<string | null> {
return localStorage.getItem('@manacore/deviceId');
},
};
return {
deviceId: getOrCreateDeviceId(),
deviceName,
deviceType,
platform: 'web',
};
},
async getStoredDeviceId(): Promise<string | null> {
return localStorage.getItem('@manacore/deviceId');
},
};
}

View file

@ -6,50 +6,50 @@ let networkAdapter: NetworkAdapter | null = null;
* Set the network adapter for the auth service
*/
export function setNetworkAdapter(adapter: NetworkAdapter): void {
networkAdapter = adapter;
networkAdapter = adapter;
}
/**
* Get the current network adapter
*/
export function getNetworkAdapter(): NetworkAdapter | null {
return networkAdapter;
return networkAdapter;
}
/**
* Check if device is connected to the network
*/
export async function isDeviceConnected(): Promise<boolean> {
if (!networkAdapter) {
// Default to true if no adapter is set
return true;
}
return networkAdapter.isDeviceConnected();
if (!networkAdapter) {
// Default to true if no adapter is set
return true;
}
return networkAdapter.isDeviceConnected();
}
/**
* Check if device has a stable connection
*/
export async function hasStableConnection(): Promise<boolean> {
if (!networkAdapter || !networkAdapter.hasStableConnection) {
// Default to basic connectivity check
return isDeviceConnected();
}
return networkAdapter.hasStableConnection();
if (!networkAdapter || !networkAdapter.hasStableConnection) {
// Default to basic connectivity check
return isDeviceConnected();
}
return networkAdapter.hasStableConnection();
}
/**
* Create a web-based network adapter
*/
export function createWebNetworkAdapter(): NetworkAdapter {
return {
async isDeviceConnected(): Promise<boolean> {
return navigator.onLine;
},
async hasStableConnection(): Promise<boolean> {
// For web, we just check online status
// More sophisticated checks could be added
return navigator.onLine;
},
};
return {
async isDeviceConnected(): Promise<boolean> {
return navigator.onLine;
},
async hasStableConnection(): Promise<boolean> {
// For web, we just check online status
// More sophisticated checks could be added
return navigator.onLine;
},
};
}

View file

@ -18,72 +18,72 @@ let storageAdapter: StorageAdapter | null = null;
* Set the storage adapter for the auth service
*/
export function setStorageAdapter(adapter: StorageAdapter): void {
storageAdapter = adapter;
storageAdapter = adapter;
}
/**
* Get the current storage adapter
*/
export function getStorageAdapter(): StorageAdapter {
if (!storageAdapter) {
throw new Error(
'Storage adapter not initialized. Call setStorageAdapter() before using auth services.'
);
}
return storageAdapter;
if (!storageAdapter) {
throw new Error(
'Storage adapter not initialized. Call setStorageAdapter() before using auth services.'
);
}
return storageAdapter;
}
/**
* Check if storage adapter is initialized
*/
export function isStorageInitialized(): boolean {
return storageAdapter !== null;
return storageAdapter !== null;
}
/**
* Create a localStorage-based storage adapter (for web)
*/
export function createLocalStorageAdapter(): StorageAdapter {
return {
async getItem<T = string>(key: string): Promise<T | null> {
const value = localStorage.getItem(key);
if (value === null) return null;
try {
return JSON.parse(value) as T;
} catch {
return value as T;
}
},
async setItem(key: string, value: string): Promise<void> {
localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
localStorage.removeItem(key);
},
};
return {
async getItem<T = string>(key: string): Promise<T | null> {
const value = localStorage.getItem(key);
if (value === null) return null;
try {
return JSON.parse(value) as T;
} catch {
return value as T;
}
},
async setItem(key: string, value: string): Promise<void> {
localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
localStorage.removeItem(key);
},
};
}
/**
* Create an in-memory storage adapter (for testing)
*/
export function createMemoryStorageAdapter(): StorageAdapter {
const storage = new Map<string, string>();
const storage = new Map<string, string>();
return {
async getItem<T = string>(key: string): Promise<T | null> {
const value = storage.get(key);
if (value === undefined) return null;
try {
return JSON.parse(value) as T;
} catch {
return value as T;
}
},
async setItem(key: string, value: string): Promise<void> {
storage.set(key, typeof value === 'string' ? value : JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
storage.delete(key);
},
};
return {
async getItem<T = string>(key: string): Promise<T | null> {
const value = storage.get(key);
if (value === undefined) return null;
try {
return JSON.parse(value) as T;
} catch {
return value as T;
}
},
async setItem(key: string, value: string): Promise<void> {
storage.set(key, typeof value === 'string' ? value : JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
storage.delete(key);
},
};
}

File diff suppressed because it is too large Load diff

View file

@ -4,157 +4,157 @@ import type { DecodedToken, UserData } from '../types';
* Decode a JWT token payload
*/
export function decodeToken(token: string): DecodedToken | null {
try {
const parts = token.split('.');
if (parts.length !== 3) {
return null;
}
try {
const parts = token.split('.');
if (parts.length !== 3) {
return null;
}
const base64Url = parts[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const base64Url = parts[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
// Add padding if needed
const padding = base64.length % 4;
const paddedBase64 = padding ? base64 + '='.repeat(4 - padding) : base64;
// Add padding if needed
const padding = base64.length % 4;
const paddedBase64 = padding ? base64 + '='.repeat(4 - padding) : base64;
// Decode base64 - atob is available in browsers, Node.js 16+, and React Native
const payload: DecodedToken = JSON.parse(atob(paddedBase64));
// Decode base64 - atob is available in browsers, Node.js 16+, and React Native
const payload: DecodedToken = JSON.parse(atob(paddedBase64));
return payload;
} catch (error) {
console.error('Error decoding JWT token:', error);
return null;
}
return payload;
} catch (error) {
console.error('Error decoding JWT token:', error);
return null;
}
}
/**
* Check if a token is valid locally (not expired)
*/
export function isTokenValidLocally(token: string, bufferSeconds: number = 10): boolean {
try {
const payload = decodeToken(token);
if (!payload || !payload.exp) {
return false;
}
try {
const payload = decodeToken(token);
if (!payload || !payload.exp) {
return false;
}
const bufferTime = bufferSeconds * 1000;
const expiryTime = payload.exp * 1000;
const currentTime = Date.now();
const bufferTime = bufferSeconds * 1000;
const expiryTime = payload.exp * 1000;
const currentTime = Date.now();
return currentTime < expiryTime - bufferTime;
} catch (error) {
console.debug('Error validating token locally:', error);
return false;
}
return currentTime < expiryTime - bufferTime;
} catch (error) {
console.debug('Error validating token locally:', error);
return false;
}
}
/**
* Check if a token is expired
*/
export function isTokenExpired(token: string): boolean {
return !isTokenValidLocally(token, 0);
return !isTokenValidLocally(token, 0);
}
/**
* Extract user data from a JWT token
*/
export function getUserFromToken(token: string, storedEmail?: string): UserData | null {
try {
const payload = decodeToken(token);
if (!payload) {
return null;
}
try {
const payload = decodeToken(token);
if (!payload) {
return null;
}
// Get email from various sources
let email = payload.email || '';
if (!email && payload.user_metadata?.email) {
email = payload.user_metadata.email;
}
if (!email && storedEmail) {
email = storedEmail;
}
// Get email from various sources
let email = payload.email || '';
if (!email && payload.user_metadata?.email) {
email = payload.user_metadata.email;
}
if (!email && storedEmail) {
email = storedEmail;
}
return {
id: payload.sub,
email: email || 'user@example.com',
role: payload.role || 'user',
};
} catch (error) {
console.error('Error extracting user from token:', error);
return null;
}
return {
id: payload.sub,
email: email || 'user@example.com',
role: payload.role || 'user',
};
} catch (error) {
console.error('Error extracting user from token:', error);
return null;
}
}
/**
* Get token expiration time in milliseconds
*/
export function getTokenExpirationTime(token: string): number | null {
const payload = decodeToken(token);
if (!payload || !payload.exp) {
return null;
}
return payload.exp * 1000;
const payload = decodeToken(token);
if (!payload || !payload.exp) {
return null;
}
return payload.exp * 1000;
}
/**
* Get time until token expiration in milliseconds
*/
export function getTimeUntilExpiration(token: string): number {
const expirationTime = getTokenExpirationTime(token);
if (!expirationTime) {
return 0;
}
return Math.max(0, expirationTime - Date.now());
const expirationTime = getTokenExpirationTime(token);
if (!expirationTime) {
return 0;
}
return Math.max(0, expirationTime - Date.now());
}
/**
* Check if user is B2B based on JWT claims
*/
export function isB2BUser(token: string): boolean {
const payload = decodeToken(token);
if (!payload) {
return false;
}
const payload = decodeToken(token);
if (!payload) {
return false;
}
// Handle different types for is_b2b
return payload.is_b2b === true || payload.is_b2b === 'true' || payload.is_b2b === 1;
// Handle different types for is_b2b
return payload.is_b2b === true || payload.is_b2b === 'true' || payload.is_b2b === 1;
}
/**
* Get B2B information from JWT claims
*/
export function getB2BInfo(token: string): {
disableRevenueCat: boolean;
organizationId?: string;
plan?: string;
role?: string;
disableRevenueCat: boolean;
organizationId?: string;
plan?: string;
role?: string;
} | null {
const payload = decodeToken(token);
if (!payload?.app_settings?.b2b) {
return null;
}
const payload = decodeToken(token);
if (!payload?.app_settings?.b2b) {
return null;
}
const b2bSettings = payload.app_settings.b2b;
return {
disableRevenueCat: !!b2bSettings.disableRevenueCat,
organizationId: b2bSettings.organizationId,
plan: b2bSettings.plan,
role: b2bSettings.role,
};
const b2bSettings = payload.app_settings.b2b;
return {
disableRevenueCat: !!b2bSettings.disableRevenueCat,
organizationId: b2bSettings.organizationId,
plan: b2bSettings.plan,
role: b2bSettings.role,
};
}
/**
* Check if RevenueCat should be disabled for this token
*/
export function shouldDisableRevenueCat(token: string): boolean {
const b2bInfo = getB2BInfo(token);
return b2bInfo?.disableRevenueCat ?? false;
const b2bInfo = getB2BInfo(token);
return b2bInfo?.disableRevenueCat ?? false;
}
/**
* Get app settings from JWT claims
*/
export function getAppSettings(token: string): Record<string, unknown> | null {
const payload = decodeToken(token);
return payload?.app_settings || null;
const payload = decodeToken(token);
return payload?.app_settings || null;
}

View file

@ -1,8 +1,8 @@
import type {
TokenState,
TokenStateObserver,
QueuedRequest,
InternalTokenRefreshResult,
TokenState,
TokenStateObserver,
QueuedRequest,
InternalTokenRefreshResult,
} from '../types';
import { TokenState as TokenStateEnum } from '../types';
import { isDeviceConnected, hasStableConnection } from '../adapters/network';
@ -12,450 +12,459 @@ import type { AuthService } from './authService';
* Configuration for the token manager
*/
export interface TokenManagerConfig {
maxQueueSize?: number;
queueTimeoutMs?: number;
maxRefreshAttempts?: number;
refreshCooldownMs?: number;
maxQueueSize?: number;
queueTimeoutMs?: number;
maxRefreshAttempts?: number;
refreshCooldownMs?: number;
}
/**
* Create a token manager instance
*/
export function createTokenManager(authService: AuthService, config?: TokenManagerConfig) {
// Configuration
const MAX_QUEUE_SIZE = config?.maxQueueSize ?? 50;
const QUEUE_TIMEOUT_MS = config?.queueTimeoutMs ?? 30000;
const MAX_REFRESH_ATTEMPTS = config?.maxRefreshAttempts ?? 3;
const REFRESH_COOLDOWN_MS = config?.refreshCooldownMs ?? 5000;
// State
let state: TokenState = TokenStateEnum.IDLE;
let refreshPromise: Promise<InternalTokenRefreshResult> | null = null;
let requestQueue: QueuedRequest[] = [];
const observers = new Set<TokenStateObserver>();
let refreshAttempts = 0;
let lastRefreshTime = 0;
// Internal functions
function notifyObservers(newState: TokenState, token?: string): void {
observers.forEach((observer) => {
try {
observer(newState, token);
} catch (error) {
console.debug('Error in token state observer:', error);
}
});
}
function setState(newState: TokenState, token?: string): void {
if (state !== newState) {
console.debug(`TokenManager: State transition ${state} -> ${newState}`);
state = newState;
notifyObservers(newState, token);
}
}
function removeFromQueue(requestId: string): void {
const index = requestQueue.findIndex((item) => item.id === requestId);
if (index !== -1) {
requestQueue.splice(index, 1);
}
}
function isRecoverableError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const networkErrors = [
'network', 'Network', 'fetch', 'connection', 'timeout',
'Failed to fetch', 'NetworkError', 'TypeError', 'ERR_NETWORK',
'ERR_INTERNET_DISCONNECTED', 'ECONNREFUSED', 'ENOTFOUND',
'ETIMEDOUT', 'Unable to resolve host', 'Request failed',
];
const authErrors = [
'401', '403', 'Unauthorized', 'Forbidden', 'Invalid token',
'Token expired', 'jwt expired', 'jwt malformed',
];
const errorString = `${error.message} ${error.name}`.toLowerCase();
const isNetworkError = networkErrors.some((keyword) =>
errorString.includes(keyword.toLowerCase())
);
const isAuthError = authErrors.some((keyword) =>
errorString.includes(keyword.toLowerCase())
);
return isNetworkError && !isAuthError;
}
async function handleRefreshFailure(): Promise<void> {
console.debug('TokenManager: Handling permanent refresh failure');
try {
await authService.clearAuthStorage();
setState(TokenStateEnum.EXPIRED);
} catch (error) {
console.debug('Error in handleRefreshFailure:', error);
}
}
async function performTokenRefresh(): Promise<InternalTokenRefreshResult> {
try {
console.debug('TokenManager: Starting token refresh');
const isOnline = await isDeviceConnected();
if (!isOnline) {
console.debug('TokenManager: Device offline, skipping refresh');
const currentToken = await authService.getAppToken();
if (currentToken) {
setState(TokenStateEnum.EXPIRED_OFFLINE, currentToken);
}
return { success: false, error: 'offline', shouldPreserveAuth: true };
}
const isStable = await hasStableConnection();
if (!isStable) {
console.debug('TokenManager: Connection not stable yet, will retry');
return { success: false, error: 'unstable_connection' };
}
const refreshToken = await authService.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const refreshResult = await authService.refreshTokens(refreshToken);
const { appToken } = refreshResult;
console.debug('TokenManager: Token refresh successful');
return { success: true, token: appToken };
} catch (error) {
console.debug('TokenManager: Token refresh failed:', error);
const isRecoverable = isRecoverableError(error);
if (!isRecoverable) {
await handleRefreshFailure();
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown refresh error',
};
}
}
async function performTokenRefreshWithRetry(): Promise<InternalTokenRefreshResult> {
const retryDelays = [0, 1000, 2000, 5000];
let lastError: unknown = null;
for (let attempt = 0; attempt < retryDelays.length; attempt++) {
try {
if (retryDelays[attempt] > 0) {
console.debug(
`TokenManager: Retrying token refresh in ${retryDelays[attempt]}ms (attempt ${attempt + 1}/${retryDelays.length})`
);
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
}
const result = await performTokenRefresh();
if (result.success) {
return result;
}
// Non-retryable errors
if (
result.error === 'invalid_token' ||
result.error === 'token_expired' ||
result.error?.includes('Device ID has changed')
) {
return result;
}
if (result.error === 'offline') {
return { success: false, error: 'offline', shouldPreserveAuth: true };
}
if (result.error === 'unstable_connection') {
await new Promise((resolve) => setTimeout(resolve, 2000));
}
lastError = new Error(result.error || 'Token refresh failed');
if (attempt === retryDelays.length - 1) break;
} catch (error) {
lastError = error;
const isRecoverable = isRecoverableError(error);
if (!isRecoverable || attempt === retryDelays.length - 1) {
break;
}
}
}
return {
success: false,
error: lastError instanceof Error ? lastError.message : 'All retry attempts failed',
};
}
async function processQueuedRequests(token: string): Promise<void> {
console.debug(`TokenManager: Processing ${requestQueue.length} queued requests`);
const requests = [...requestQueue];
requestQueue = [];
for (const request of requests) {
try {
const response = await retryRequestWithToken(request.input, request.init, token);
request.resolve(response);
} catch (error) {
request.reject(error);
}
}
}
async function rejectQueuedRequests(error: string): Promise<void> {
console.debug(`TokenManager: Rejecting ${requestQueue.length} queued requests`);
const requests = [...requestQueue];
requestQueue = [];
for (const request of requests) {
request.reject(new Error(error));
}
}
async function retryRequestWithToken(
input: RequestInfo | URL,
init: RequestInit | undefined,
token: string
): Promise<Response> {
const headers = new Headers(init?.headers || {});
headers.set('Authorization', `Bearer ${token}`);
return fetch(input, {
...init,
headers,
});
}
// Public API
const manager = {
/**
* Subscribe to token state changes
*/
subscribe(observer: TokenStateObserver): () => void {
observers.add(observer);
return () => observers.delete(observer);
},
/**
* Get current token state
*/
getState(): TokenState {
return state;
},
/**
* Get a valid token, refreshing if necessary
*/
async getValidToken(): Promise<string | null> {
const currentToken = await authService.getAppToken();
if (currentToken && authService.isTokenValidLocally(currentToken)) {
setState(TokenStateEnum.VALID, currentToken);
return currentToken;
}
if (!currentToken) {
console.debug('TokenManager: No token available, skipping refresh');
setState(TokenStateEnum.EXPIRED);
return null;
}
const isOnline = await isDeviceConnected();
if (!isOnline) {
console.debug('TokenManager: Token expired while offline');
setState(TokenStateEnum.EXPIRED_OFFLINE, currentToken);
return currentToken;
}
const refreshResult = await manager.refreshToken();
if (refreshResult.success && refreshResult.token) {
return refreshResult.token;
}
if (refreshResult.shouldPreserveAuth) {
setState(TokenStateEnum.EXPIRED_OFFLINE, currentToken);
return currentToken;
}
return null;
},
/**
* Handle 401 response
*/
async handle401Response(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
if (state === TokenStateEnum.REFRESHING && refreshPromise) {
return manager.queueRequest(input, init);
}
const refreshResult = await manager.refreshToken();
if (refreshResult.success && refreshResult.token) {
return retryRequestWithToken(input, init, refreshResult.token);
}
throw new Error(refreshResult.error || 'Token refresh failed');
},
/**
* Queue a request during token refresh
*/
async queueRequest(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
return new Promise((resolve, reject) => {
if (requestQueue.length >= MAX_QUEUE_SIZE) {
reject(new Error('Request queue full'));
return;
}
const queueItem: QueuedRequest = {
id: Math.random().toString(36).substring(2, 11),
input,
init,
resolve,
reject,
timestamp: Date.now(),
};
requestQueue.push(queueItem);
setTimeout(() => {
removeFromQueue(queueItem.id);
reject(new Error('Queued request timeout'));
}, QUEUE_TIMEOUT_MS);
});
},
/**
* Refresh the authentication token
*/
async refreshToken(): Promise<InternalTokenRefreshResult> {
const now = Date.now();
if (now - lastRefreshTime < REFRESH_COOLDOWN_MS) {
return { success: false, error: 'Refresh cooldown active' };
}
if (refreshAttempts >= MAX_REFRESH_ATTEMPTS) {
await handleRefreshFailure();
return { success: false, error: 'Max refresh attempts reached' };
}
if (refreshPromise) {
return refreshPromise;
}
setState(TokenStateEnum.REFRESHING);
lastRefreshTime = now;
refreshPromise = performTokenRefreshWithRetry();
try {
const result = await refreshPromise;
if (result.success) {
refreshAttempts = 0;
setState(TokenStateEnum.VALID, result.token);
await processQueuedRequests(result.token!);
} else {
refreshAttempts++;
setState(TokenStateEnum.EXPIRED);
await rejectQueuedRequests(result.error || 'Token refresh failed');
}
return result;
} finally {
refreshPromise = null;
}
},
/**
* Reset the token manager state
*/
reset(): void {
state = TokenStateEnum.IDLE;
refreshPromise = null;
refreshAttempts = 0;
lastRefreshTime = 0;
const requests = [...requestQueue];
requestQueue = [];
for (const request of requests) {
request.reject(new Error('Token manager reset'));
}
},
/**
* Clear tokens and reset state
*/
async clearTokens(): Promise<void> {
try {
await authService.clearAuthStorage();
manager.reset();
} catch (error) {
console.debug('Error clearing tokens:', error);
manager.reset();
}
},
/**
* Get queue status for debugging
*/
getQueueStatus(): { size: number; state: TokenState; refreshAttempts: number } {
return {
size: requestQueue.length,
state,
refreshAttempts,
};
},
/**
* Check initial token state
*/
async checkInitialState(): Promise<void> {
try {
const token = await authService.getAppToken();
if (!token) {
setState(TokenStateEnum.EXPIRED);
return;
}
if (authService.isTokenValidLocally(token)) {
setState(TokenStateEnum.VALID, token);
} else {
setState(TokenStateEnum.EXPIRED);
}
} catch (error) {
console.debug('Error checking initial token state:', error);
setState(TokenStateEnum.EXPIRED);
}
},
};
// Initialize
manager.checkInitialState();
return manager;
// Configuration
const MAX_QUEUE_SIZE = config?.maxQueueSize ?? 50;
const QUEUE_TIMEOUT_MS = config?.queueTimeoutMs ?? 30000;
const MAX_REFRESH_ATTEMPTS = config?.maxRefreshAttempts ?? 3;
const REFRESH_COOLDOWN_MS = config?.refreshCooldownMs ?? 5000;
// State
let state: TokenState = TokenStateEnum.IDLE;
let refreshPromise: Promise<InternalTokenRefreshResult> | null = null;
let requestQueue: QueuedRequest[] = [];
const observers = new Set<TokenStateObserver>();
let refreshAttempts = 0;
let lastRefreshTime = 0;
// Internal functions
function notifyObservers(newState: TokenState, token?: string): void {
observers.forEach((observer) => {
try {
observer(newState, token);
} catch (error) {
console.debug('Error in token state observer:', error);
}
});
}
function setState(newState: TokenState, token?: string): void {
if (state !== newState) {
console.debug(`TokenManager: State transition ${state} -> ${newState}`);
state = newState;
notifyObservers(newState, token);
}
}
function removeFromQueue(requestId: string): void {
const index = requestQueue.findIndex((item) => item.id === requestId);
if (index !== -1) {
requestQueue.splice(index, 1);
}
}
function isRecoverableError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const networkErrors = [
'network',
'Network',
'fetch',
'connection',
'timeout',
'Failed to fetch',
'NetworkError',
'TypeError',
'ERR_NETWORK',
'ERR_INTERNET_DISCONNECTED',
'ECONNREFUSED',
'ENOTFOUND',
'ETIMEDOUT',
'Unable to resolve host',
'Request failed',
];
const authErrors = [
'401',
'403',
'Unauthorized',
'Forbidden',
'Invalid token',
'Token expired',
'jwt expired',
'jwt malformed',
];
const errorString = `${error.message} ${error.name}`.toLowerCase();
const isNetworkError = networkErrors.some((keyword) =>
errorString.includes(keyword.toLowerCase())
);
const isAuthError = authErrors.some((keyword) => errorString.includes(keyword.toLowerCase()));
return isNetworkError && !isAuthError;
}
async function handleRefreshFailure(): Promise<void> {
console.debug('TokenManager: Handling permanent refresh failure');
try {
await authService.clearAuthStorage();
setState(TokenStateEnum.EXPIRED);
} catch (error) {
console.debug('Error in handleRefreshFailure:', error);
}
}
async function performTokenRefresh(): Promise<InternalTokenRefreshResult> {
try {
console.debug('TokenManager: Starting token refresh');
const isOnline = await isDeviceConnected();
if (!isOnline) {
console.debug('TokenManager: Device offline, skipping refresh');
const currentToken = await authService.getAppToken();
if (currentToken) {
setState(TokenStateEnum.EXPIRED_OFFLINE, currentToken);
}
return { success: false, error: 'offline', shouldPreserveAuth: true };
}
const isStable = await hasStableConnection();
if (!isStable) {
console.debug('TokenManager: Connection not stable yet, will retry');
return { success: false, error: 'unstable_connection' };
}
const refreshToken = await authService.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const refreshResult = await authService.refreshTokens(refreshToken);
const { appToken } = refreshResult;
console.debug('TokenManager: Token refresh successful');
return { success: true, token: appToken };
} catch (error) {
console.debug('TokenManager: Token refresh failed:', error);
const isRecoverable = isRecoverableError(error);
if (!isRecoverable) {
await handleRefreshFailure();
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown refresh error',
};
}
}
async function performTokenRefreshWithRetry(): Promise<InternalTokenRefreshResult> {
const retryDelays = [0, 1000, 2000, 5000];
let lastError: unknown = null;
for (let attempt = 0; attempt < retryDelays.length; attempt++) {
try {
if (retryDelays[attempt] > 0) {
console.debug(
`TokenManager: Retrying token refresh in ${retryDelays[attempt]}ms (attempt ${attempt + 1}/${retryDelays.length})`
);
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
}
const result = await performTokenRefresh();
if (result.success) {
return result;
}
// Non-retryable errors
if (
result.error === 'invalid_token' ||
result.error === 'token_expired' ||
result.error?.includes('Device ID has changed')
) {
return result;
}
if (result.error === 'offline') {
return { success: false, error: 'offline', shouldPreserveAuth: true };
}
if (result.error === 'unstable_connection') {
await new Promise((resolve) => setTimeout(resolve, 2000));
}
lastError = new Error(result.error || 'Token refresh failed');
if (attempt === retryDelays.length - 1) break;
} catch (error) {
lastError = error;
const isRecoverable = isRecoverableError(error);
if (!isRecoverable || attempt === retryDelays.length - 1) {
break;
}
}
}
return {
success: false,
error: lastError instanceof Error ? lastError.message : 'All retry attempts failed',
};
}
async function processQueuedRequests(token: string): Promise<void> {
console.debug(`TokenManager: Processing ${requestQueue.length} queued requests`);
const requests = [...requestQueue];
requestQueue = [];
for (const request of requests) {
try {
const response = await retryRequestWithToken(request.input, request.init, token);
request.resolve(response);
} catch (error) {
request.reject(error);
}
}
}
async function rejectQueuedRequests(error: string): Promise<void> {
console.debug(`TokenManager: Rejecting ${requestQueue.length} queued requests`);
const requests = [...requestQueue];
requestQueue = [];
for (const request of requests) {
request.reject(new Error(error));
}
}
async function retryRequestWithToken(
input: RequestInfo | URL,
init: RequestInit | undefined,
token: string
): Promise<Response> {
const headers = new Headers(init?.headers || {});
headers.set('Authorization', `Bearer ${token}`);
return fetch(input, {
...init,
headers,
});
}
// Public API
const manager = {
/**
* Subscribe to token state changes
*/
subscribe(observer: TokenStateObserver): () => void {
observers.add(observer);
return () => observers.delete(observer);
},
/**
* Get current token state
*/
getState(): TokenState {
return state;
},
/**
* Get a valid token, refreshing if necessary
*/
async getValidToken(): Promise<string | null> {
const currentToken = await authService.getAppToken();
if (currentToken && authService.isTokenValidLocally(currentToken)) {
setState(TokenStateEnum.VALID, currentToken);
return currentToken;
}
if (!currentToken) {
console.debug('TokenManager: No token available, skipping refresh');
setState(TokenStateEnum.EXPIRED);
return null;
}
const isOnline = await isDeviceConnected();
if (!isOnline) {
console.debug('TokenManager: Token expired while offline');
setState(TokenStateEnum.EXPIRED_OFFLINE, currentToken);
return currentToken;
}
const refreshResult = await manager.refreshToken();
if (refreshResult.success && refreshResult.token) {
return refreshResult.token;
}
if (refreshResult.shouldPreserveAuth) {
setState(TokenStateEnum.EXPIRED_OFFLINE, currentToken);
return currentToken;
}
return null;
},
/**
* Handle 401 response
*/
async handle401Response(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
if (state === TokenStateEnum.REFRESHING && refreshPromise) {
return manager.queueRequest(input, init);
}
const refreshResult = await manager.refreshToken();
if (refreshResult.success && refreshResult.token) {
return retryRequestWithToken(input, init, refreshResult.token);
}
throw new Error(refreshResult.error || 'Token refresh failed');
},
/**
* Queue a request during token refresh
*/
async queueRequest(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return new Promise((resolve, reject) => {
if (requestQueue.length >= MAX_QUEUE_SIZE) {
reject(new Error('Request queue full'));
return;
}
const queueItem: QueuedRequest = {
id: Math.random().toString(36).substring(2, 11),
input,
init,
resolve,
reject,
timestamp: Date.now(),
};
requestQueue.push(queueItem);
setTimeout(() => {
removeFromQueue(queueItem.id);
reject(new Error('Queued request timeout'));
}, QUEUE_TIMEOUT_MS);
});
},
/**
* Refresh the authentication token
*/
async refreshToken(): Promise<InternalTokenRefreshResult> {
const now = Date.now();
if (now - lastRefreshTime < REFRESH_COOLDOWN_MS) {
return { success: false, error: 'Refresh cooldown active' };
}
if (refreshAttempts >= MAX_REFRESH_ATTEMPTS) {
await handleRefreshFailure();
return { success: false, error: 'Max refresh attempts reached' };
}
if (refreshPromise) {
return refreshPromise;
}
setState(TokenStateEnum.REFRESHING);
lastRefreshTime = now;
refreshPromise = performTokenRefreshWithRetry();
try {
const result = await refreshPromise;
if (result.success) {
refreshAttempts = 0;
setState(TokenStateEnum.VALID, result.token);
await processQueuedRequests(result.token!);
} else {
refreshAttempts++;
setState(TokenStateEnum.EXPIRED);
await rejectQueuedRequests(result.error || 'Token refresh failed');
}
return result;
} finally {
refreshPromise = null;
}
},
/**
* Reset the token manager state
*/
reset(): void {
state = TokenStateEnum.IDLE;
refreshPromise = null;
refreshAttempts = 0;
lastRefreshTime = 0;
const requests = [...requestQueue];
requestQueue = [];
for (const request of requests) {
request.reject(new Error('Token manager reset'));
}
},
/**
* Clear tokens and reset state
*/
async clearTokens(): Promise<void> {
try {
await authService.clearAuthStorage();
manager.reset();
} catch (error) {
console.debug('Error clearing tokens:', error);
manager.reset();
}
},
/**
* Get queue status for debugging
*/
getQueueStatus(): { size: number; state: TokenState; refreshAttempts: number } {
return {
size: requestQueue.length,
state,
refreshAttempts,
};
},
/**
* Check initial token state
*/
async checkInitialState(): Promise<void> {
try {
const token = await authService.getAppToken();
if (!token) {
setState(TokenStateEnum.EXPIRED);
return;
}
if (authService.isTokenValidLocally(token)) {
setState(TokenStateEnum.VALID, token);
} else {
setState(TokenStateEnum.EXPIRED);
}
} catch (error) {
console.debug('Error checking initial token state:', error);
setState(TokenStateEnum.EXPIRED);
}
},
};
// Initialize
manager.checkInitialState();
return manager;
}
/**

View file

@ -11,62 +11,62 @@ export { createTokenManager } from './core/tokenManager';
export type { TokenManager, TokenManagerConfig } from './core/tokenManager';
export {
decodeToken,
isTokenValidLocally,
isTokenExpired,
getUserFromToken,
getTokenExpirationTime,
getTimeUntilExpiration,
isB2BUser,
getB2BInfo,
shouldDisableRevenueCat,
getAppSettings,
decodeToken,
isTokenValidLocally,
isTokenExpired,
getUserFromToken,
getTokenExpirationTime,
getTimeUntilExpiration,
isB2BUser,
getB2BInfo,
shouldDisableRevenueCat,
getAppSettings,
} from './core/jwtUtils';
// Storage adapter
import {
setStorageAdapter as _setStorageAdapter,
createLocalStorageAdapter as _createLocalStorageAdapter,
setStorageAdapter as _setStorageAdapter,
createLocalStorageAdapter as _createLocalStorageAdapter,
} from './adapters/storage';
export {
setStorageAdapter,
getStorageAdapter,
isStorageInitialized,
createLocalStorageAdapter,
createMemoryStorageAdapter,
setStorageAdapter,
getStorageAdapter,
isStorageInitialized,
createLocalStorageAdapter,
createMemoryStorageAdapter,
} from './adapters/storage';
// Device adapter
import {
setDeviceAdapter as _setDeviceAdapter,
createWebDeviceAdapter as _createWebDeviceAdapter,
setDeviceAdapter as _setDeviceAdapter,
createWebDeviceAdapter as _createWebDeviceAdapter,
} from './adapters/device';
export {
setDeviceAdapter,
getDeviceAdapter,
isDeviceInitialized,
createWebDeviceAdapter,
setDeviceAdapter,
getDeviceAdapter,
isDeviceInitialized,
createWebDeviceAdapter,
} from './adapters/device';
// Network adapter
import {
setNetworkAdapter as _setNetworkAdapter,
createWebNetworkAdapter as _createWebNetworkAdapter,
setNetworkAdapter as _setNetworkAdapter,
createWebNetworkAdapter as _createWebNetworkAdapter,
} from './adapters/network';
export {
setNetworkAdapter,
getNetworkAdapter,
isDeviceConnected,
hasStableConnection,
createWebNetworkAdapter,
setNetworkAdapter,
getNetworkAdapter,
isDeviceConnected,
hasStableConnection,
createWebNetworkAdapter,
} from './adapters/network';
// Fetch interceptor
import { setupFetchInterceptor as _setupFetchInterceptor } from './interceptors/fetchInterceptor';
export {
setupFetchInterceptor,
setupTokenObservers,
getInterceptorStatus,
setupFetchInterceptor,
setupTokenObservers,
getInterceptorStatus,
} from './interceptors/fetchInterceptor';
export type { FetchInterceptorConfig } from './interceptors/fetchInterceptor';
@ -82,18 +82,21 @@ export type { FetchInterceptorConfig } from './interceptors/fetchInterceptor';
* });
* ```
*/
export function initializeWebAuth(config: { baseUrl: string; storageKeys?: Partial<import('./types').StorageKeys> }) {
// Set up adapters
_setStorageAdapter(_createLocalStorageAdapter());
_setDeviceAdapter(_createWebDeviceAdapter());
_setNetworkAdapter(_createWebNetworkAdapter());
export function initializeWebAuth(config: {
baseUrl: string;
storageKeys?: Partial<import('./types').StorageKeys>;
}) {
// Set up adapters
_setStorageAdapter(_createLocalStorageAdapter());
_setDeviceAdapter(_createWebDeviceAdapter());
_setNetworkAdapter(_createWebNetworkAdapter());
// Create services
const authService = _createAuthService(config);
const tokenManager = _createTokenManager(authService);
// Create services
const authService = _createAuthService(config);
const tokenManager = _createTokenManager(authService);
// Set up interceptor
_setupFetchInterceptor(authService, tokenManager);
// Set up interceptor
_setupFetchInterceptor(authService, tokenManager);
return { authService, tokenManager };
return { authService, tokenManager };
}

View file

@ -6,215 +6,212 @@ import { TokenState } from '../types';
* Configuration for the fetch interceptor
*/
export interface FetchInterceptorConfig {
/**
* Patterns to skip (won't be intercepted)
*/
skipPatterns?: string[];
/**
* Backend URL to match (only intercept requests to this URL)
*/
backendUrl?: string;
/**
* Patterns to skip (won't be intercepted)
*/
skipPatterns?: string[];
/**
* Backend URL to match (only intercept requests to this URL)
*/
backendUrl?: string;
}
/**
* Default patterns to skip
*/
const DEFAULT_SKIP_PATTERNS = [
// Auth endpoints
'/auth/signin',
'/auth/signup',
'/auth/refresh',
'/auth/forgot-password',
'/auth/reset-password',
'/auth/verify',
'/auth/logout',
// Public endpoints
'/health',
'/ping',
'/status',
'/version',
'/public/',
// Storage endpoints
'.supabase.co/storage/',
'/storage/v1/',
// External APIs
'googleapis.com',
'firebase.com',
'firebaseapp.com',
'replicate.com',
'openai.com',
'anthropic.com',
// Auth endpoints
'/auth/signin',
'/auth/signup',
'/auth/refresh',
'/auth/forgot-password',
'/auth/reset-password',
'/auth/verify',
'/auth/logout',
// Public endpoints
'/health',
'/ping',
'/status',
'/version',
'/public/',
// Storage endpoints
'.supabase.co/storage/',
'/storage/v1/',
// External APIs
'googleapis.com',
'firebase.com',
'firebaseapp.com',
'replicate.com',
'openai.com',
'anthropic.com',
];
/**
* Setup a global fetch interceptor for automatic token handling
*/
export function setupFetchInterceptor(
authService: AuthService,
tokenManager: TokenManager,
config?: FetchInterceptorConfig
authService: AuthService,
tokenManager: TokenManager,
config?: FetchInterceptorConfig
): void {
if (typeof globalThis === 'undefined' || !globalThis.fetch) {
console.warn('FetchInterceptor: globalThis.fetch not available');
return;
}
if (typeof globalThis === 'undefined' || !globalThis.fetch) {
console.warn('FetchInterceptor: globalThis.fetch not available');
return;
}
const originalFetch = globalThis.fetch;
const skipPatterns = [...DEFAULT_SKIP_PATTERNS, ...(config?.skipPatterns || [])];
const backendUrl = config?.backendUrl || authService.getBaseUrl();
const originalFetch = globalThis.fetch;
const skipPatterns = [...DEFAULT_SKIP_PATTERNS, ...(config?.skipPatterns || [])];
const backendUrl = config?.backendUrl || authService.getBaseUrl();
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = extractUrl(input);
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = extractUrl(input);
// Skip intercepting if URL doesn't match criteria
if (shouldSkipInterception(url, skipPatterns, backendUrl)) {
return originalFetch(input, init);
}
// Skip intercepting if URL doesn't match criteria
if (shouldSkipInterception(url, skipPatterns, backendUrl)) {
return originalFetch(input, init);
}
console.debug('Fetch interceptor: Intercepting URL:', url);
console.debug('Fetch interceptor: Intercepting URL:', url);
try {
// Make request with current token
const response = await makeRequestWithToken(originalFetch, authService, input, init);
try {
// Make request with current token
const response = await makeRequestWithToken(originalFetch, authService, input, init);
// Handle 401 responses
if (response.status === 401) {
const responseData = await response.clone().json().catch(() => ({}));
console.debug('Fetch interceptor: Received 401 response:', responseData);
// Handle 401 responses
if (response.status === 401) {
const responseData = await response
.clone()
.json()
.catch(() => ({}));
console.debug('Fetch interceptor: Received 401 response:', responseData);
if (isTokenExpiredResponse(responseData)) {
console.debug('Fetch interceptor: Token expired, delegating to TokenManager');
return tokenManager.handle401Response(input, init);
}
}
if (isTokenExpiredResponse(responseData)) {
console.debug('Fetch interceptor: Token expired, delegating to TokenManager');
return tokenManager.handle401Response(input, init);
}
}
return response;
} catch (error) {
console.debug('Error in global fetch interceptor:', error);
return originalFetch(input, init);
}
}) as typeof fetch;
return response;
} catch (error) {
console.debug('Error in global fetch interceptor:', error);
return originalFetch(input, init);
}
}) as typeof fetch;
}
/**
* Setup token state observers for integrations (e.g., Supabase)
*/
export function setupTokenObservers(
tokenManager: TokenManager,
onValid?: (token: string) => void | Promise<void>,
onExpired?: () => void | Promise<void>
tokenManager: TokenManager,
onValid?: (token: string) => void | Promise<void>,
onExpired?: () => void | Promise<void>
): () => void {
return tokenManager.subscribe(async (state, token) => {
try {
if (state === TokenState.VALID && token && onValid) {
await onValid(token);
} else if (state === TokenState.EXPIRED && onExpired) {
await onExpired();
}
} catch (error) {
console.debug('Error in token observer:', error);
}
});
return tokenManager.subscribe(async (state, token) => {
try {
if (state === TokenState.VALID && token && onValid) {
await onValid(token);
} else if (state === TokenState.EXPIRED && onExpired) {
await onExpired();
}
} catch (error) {
console.debug('Error in token observer:', error);
}
});
}
/**
* Extract URL from various input types
*/
function extractUrl(input: RequestInfo | URL): string {
if (typeof input === 'string') {
return input;
} else if (input instanceof URL) {
return input.toString();
} else if (input instanceof Request) {
return input.url;
}
return '';
if (typeof input === 'string') {
return input;
} else if (input instanceof URL) {
return input.toString();
} else if (input instanceof Request) {
return input.url;
}
return '';
}
/**
* Check if request should skip interception
*/
function shouldSkipInterception(
url: string,
skipPatterns: string[],
backendUrl: string
): boolean {
if (!url) return true;
function shouldSkipInterception(url: string, skipPatterns: string[], backendUrl: string): boolean {
if (!url) return true;
const lowerUrl = url.toLowerCase();
const lowerUrl = url.toLowerCase();
// Check skip patterns
if (skipPatterns.some((pattern) => lowerUrl.includes(pattern.toLowerCase()))) {
return true;
}
// Check skip patterns
if (skipPatterns.some((pattern) => lowerUrl.includes(pattern.toLowerCase()))) {
return true;
}
// Check if URL matches backend
const backendDomain = backendUrl
.replace(/https?:\/\//, '')
.replace(/:\d+$/, '')
.toLowerCase();
// Check if URL matches backend
const backendDomain = backendUrl
.replace(/https?:\/\//, '')
.replace(/:\d+$/, '')
.toLowerCase();
if (!lowerUrl.includes(backendDomain)) {
return true;
}
if (!lowerUrl.includes(backendDomain)) {
return true;
}
return false;
return false;
}
/**
* Make a request with the current token
*/
async function makeRequestWithToken(
originalFetch: typeof fetch,
authService: AuthService,
input: RequestInfo | URL,
init?: RequestInit
originalFetch: typeof fetch,
authService: AuthService,
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
const token = await authService.getAppToken();
const token = await authService.getAppToken();
const requestInit: RequestInit = {
method: init?.method || 'GET',
...init,
};
const requestInit: RequestInit = {
method: init?.method || 'GET',
...init,
};
if (token) {
const headers = new Headers(requestInit.headers || {});
headers.set('Authorization', `Bearer ${token}`);
requestInit.headers = headers;
}
if (token) {
const headers = new Headers(requestInit.headers || {});
headers.set('Authorization', `Bearer ${token}`);
requestInit.headers = headers;
}
return originalFetch(input, requestInit);
return originalFetch(input, requestInit);
}
/**
* Check if response indicates token expiration
*/
function isTokenExpiredResponse(responseData: Record<string, unknown>): boolean {
const error = responseData.error as Record<string, unknown> | undefined;
const errorMessage = String(error?.message || responseData.message || responseData.error || '');
const errorCode = String(responseData.code || error?.code || '');
const error = responseData.error as Record<string, unknown> | undefined;
const errorMessage = String(error?.message || responseData.message || responseData.error || '');
const errorCode = String(responseData.code || error?.code || '');
return (
errorMessage === 'JWT expired' ||
errorCode === 'PGRST301' ||
errorMessage === 'Unauthorized'
);
return (
errorMessage === 'JWT expired' || errorCode === 'PGRST301' || errorMessage === 'Unauthorized'
);
}
/**
* Get interceptor status for debugging
*/
export function getInterceptorStatus(
authService: AuthService,
tokenManager: TokenManager
authService: AuthService,
tokenManager: TokenManager
): {
isSetup: boolean;
backendUrl: string;
tokenManager: { size: number; state: string; refreshAttempts: number };
isSetup: boolean;
backendUrl: string;
tokenManager: { size: number; state: string; refreshAttempts: number };
} {
return {
isSetup: typeof globalThis !== 'undefined' && globalThis.fetch !== undefined,
backendUrl: authService.getBaseUrl(),
tokenManager: tokenManager.getQueueStatus(),
};
return {
isSetup: typeof globalThis !== 'undefined' && globalThis.fetch !== undefined,
backendUrl: authService.getBaseUrl(),
tokenManager: tokenManager.getQueueStatus(),
};
}

View file

@ -2,83 +2,83 @@
* Storage keys for authentication data
*/
export interface StorageKeys {
APP_TOKEN: string;
REFRESH_TOKEN: string;
USER_EMAIL: string;
APP_TOKEN: string;
REFRESH_TOKEN: string;
USER_EMAIL: string;
}
/**
* Device information for multi-device support
*/
export interface DeviceInfo {
deviceId: string;
deviceName: string;
deviceType: string;
platform?: string;
deviceId: string;
deviceName: string;
deviceType: string;
platform?: string;
}
/**
* Decoded JWT token payload
*/
export interface DecodedToken {
sub: string;
email?: string;
role?: string;
exp: number;
iat: number;
aud?: string;
app_id?: string;
is_b2b?: boolean | string | number;
subscription_plan_id?: string;
user_metadata?: {
email?: string;
};
app_settings?: {
b2b?: {
disableRevenueCat?: boolean;
organizationId?: string;
plan?: string;
role?: string;
};
};
sub: string;
email?: string;
role?: string;
exp: number;
iat: number;
aud?: string;
app_id?: string;
is_b2b?: boolean | string | number;
subscription_plan_id?: string;
user_metadata?: {
email?: string;
};
app_settings?: {
b2b?: {
disableRevenueCat?: boolean;
organizationId?: string;
plan?: string;
role?: string;
};
};
}
/**
* User data extracted from token
*/
export interface UserData {
id: string;
email: string;
role: string;
id: string;
email: string;
role: string;
}
/**
* Authentication result from sign in/up
*/
export interface AuthResult {
success: boolean;
error?: string;
needsVerification?: boolean;
success: boolean;
error?: string;
needsVerification?: boolean;
}
/**
* Token refresh result
*/
export interface TokenRefreshResult {
appToken: string;
refreshToken: string;
userData?: UserData | null;
appToken: string;
refreshToken: string;
userData?: UserData | null;
}
/**
* Token state for the token manager
*/
export enum TokenState {
IDLE = 'idle',
REFRESHING = 'refreshing',
EXPIRED = 'expired',
EXPIRED_OFFLINE = 'expired_offline',
VALID = 'valid',
IDLE = 'idle',
REFRESHING = 'refreshing',
EXPIRED = 'expired',
EXPIRED_OFFLINE = 'expired_offline',
VALID = 'valid',
}
/**
@ -90,89 +90,89 @@ export type TokenStateObserver = (state: TokenState, token?: string) => void;
* Queued request item during token refresh
*/
export interface QueuedRequest {
id: string;
input: RequestInfo | URL;
init?: RequestInit;
resolve: (value: Response) => void;
reject: (reason?: unknown) => void;
timestamp: number;
id: string;
input: RequestInfo | URL;
init?: RequestInit;
resolve: (value: Response) => void;
reject: (reason?: unknown) => void;
timestamp: number;
}
/**
* Internal token refresh result
*/
export interface InternalTokenRefreshResult {
success: boolean;
token?: string;
error?: string;
shouldPreserveAuth?: boolean;
shouldRetry?: boolean;
success: boolean;
token?: string;
error?: string;
shouldPreserveAuth?: boolean;
shouldRetry?: boolean;
}
/**
* Configuration for the auth service
*/
export interface AuthServiceConfig {
baseUrl: string;
storageKeys?: Partial<StorageKeys>;
endpoints?: Partial<AuthEndpoints>;
baseUrl: string;
storageKeys?: Partial<StorageKeys>;
endpoints?: Partial<AuthEndpoints>;
}
/**
* Auth API endpoints
*/
export interface AuthEndpoints {
signIn: string;
signUp: string;
signOut: string;
refresh: string;
validate: string;
forgotPassword: string;
googleSignIn: string;
appleSignIn: string;
credits: string;
signIn: string;
signUp: string;
signOut: string;
refresh: string;
validate: string;
forgotPassword: string;
googleSignIn: string;
appleSignIn: string;
credits: string;
}
/**
* Storage adapter interface
*/
export interface StorageAdapter {
getItem<T = string>(key: string): Promise<T | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
getItem<T = string>(key: string): Promise<T | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
}
/**
* Device manager adapter interface
*/
export interface DeviceManagerAdapter {
getDeviceInfo(): Promise<DeviceInfo>;
getStoredDeviceId(): Promise<string | null>;
getDeviceInfo(): Promise<DeviceInfo>;
getStoredDeviceId(): Promise<string | null>;
}
/**
* Network utilities adapter interface
*/
export interface NetworkAdapter {
isDeviceConnected(): Promise<boolean>;
hasStableConnection?(): Promise<boolean>;
isDeviceConnected(): Promise<boolean>;
hasStableConnection?(): Promise<boolean>;
}
/**
* Credit balance response
*/
export interface CreditBalance {
credits: number;
maxCreditLimit: number;
userId: string;
credits: number;
maxCreditLimit: number;
userId: string;
}
/**
* B2B information from JWT claims
*/
export interface B2BInfo {
disableRevenueCat: boolean;
organizationId?: string;
plan?: string;
role?: string;
disableRevenueCat: boolean;
organizationId?: string;
plan?: string;
role?: string;
}