mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 00:19:41 +02:00
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:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue