/** * Authentication Provider * * Manages authentication state and token lifecycle */ import type { AuthTokenResponse } from '@breakpilot/compliance-sdk-types' import { ComplianceClient } from './client' // ============================================================================= // TYPES // ============================================================================= export interface AuthProviderOptions { client: ComplianceClient clientId: string clientSecret?: string storage?: Storage onAuthStateChange?: (state: AuthState) => void } export interface AuthState { isAuthenticated: boolean accessToken: string | null refreshToken: string | null expiresAt: Date | null user?: { id: string email: string name: string role: string } } // ============================================================================= // CONSTANTS // ============================================================================= const STORAGE_KEY = 'breakpilot-sdk-auth' const TOKEN_REFRESH_BUFFER = 5 * 60 * 1000 // 5 minutes before expiry // ============================================================================= // AUTH PROVIDER // ============================================================================= export class AuthProvider { private client: ComplianceClient private clientId: string private clientSecret?: string private storage: Storage | null private state: AuthState private refreshTimeout: ReturnType | null = null private onAuthStateChange?: (state: AuthState) => void constructor(options: AuthProviderOptions) { this.client = options.client this.clientId = options.clientId this.clientSecret = options.clientSecret this.storage = options.storage ?? (typeof localStorage !== 'undefined' ? localStorage : null) this.onAuthStateChange = options.onAuthStateChange this.state = { isAuthenticated: false, accessToken: null, refreshToken: null, expiresAt: null, } this.loadFromStorage() } // --------------------------------------------------------------------------- // Private Methods // --------------------------------------------------------------------------- private loadFromStorage(): void { if (!this.storage) return try { const stored = this.storage.getItem(STORAGE_KEY) if (stored) { const data = JSON.parse(stored) if (data.expiresAt) { data.expiresAt = new Date(data.expiresAt) } // Check if token is still valid if (data.expiresAt && data.expiresAt > new Date()) { this.state = data this.client.setAccessToken(data.accessToken) this.scheduleTokenRefresh() this.notifyStateChange() } else if (data.refreshToken) { // Try to refresh this.refreshToken().catch(() => { this.clearAuth() }) } } } catch (error) { console.error('Failed to load auth from storage:', error) } } private saveToStorage(): void { if (!this.storage) return try { this.storage.setItem(STORAGE_KEY, JSON.stringify(this.state)) } catch (error) { console.error('Failed to save auth to storage:', error) } } private clearStorage(): void { if (!this.storage) return try { this.storage.removeItem(STORAGE_KEY) } catch (error) { console.error('Failed to clear auth from storage:', error) } } private updateState(tokenResponse: AuthTokenResponse): void { const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1000) this.state = { isAuthenticated: true, accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken ?? this.state.refreshToken, expiresAt, } this.client.setAccessToken(tokenResponse.accessToken) this.saveToStorage() this.scheduleTokenRefresh() this.notifyStateChange() } private scheduleTokenRefresh(): void { if (this.refreshTimeout) { clearTimeout(this.refreshTimeout) } if (!this.state.expiresAt || !this.state.refreshToken) return const timeUntilRefresh = this.state.expiresAt.getTime() - Date.now() - TOKEN_REFRESH_BUFFER if (timeUntilRefresh > 0) { this.refreshTimeout = setTimeout(() => { this.refreshToken().catch(() => { this.clearAuth() }) }, timeUntilRefresh) } } private notifyStateChange(): void { this.onAuthStateChange?.(this.state) } // --------------------------------------------------------------------------- // Public Methods // --------------------------------------------------------------------------- /** * Authenticate using client credentials (server-to-server) */ async authenticateWithCredentials(): Promise { if (!this.clientSecret) { throw new Error('Client secret is required for credentials authentication') } const response = await this.client.authenticate({ grantType: 'client_credentials', clientId: this.clientId, clientSecret: this.clientSecret, }) this.updateState(response) } /** * Authenticate using authorization code (OAuth flow) */ async authenticateWithCode(code: string, redirectUri: string): Promise { const response = await this.client.authenticate({ grantType: 'authorization_code', clientId: this.clientId, clientSecret: this.clientSecret, code, redirectUri, }) this.updateState(response) } /** * Refresh the access token */ async refreshToken(): Promise { if (!this.state.refreshToken) { throw new Error('No refresh token available') } const response = await this.client.refreshToken(this.state.refreshToken) this.updateState(response) } /** * Set access token manually (e.g., from API key) */ setAccessToken(token: string, expiresIn?: number): void { const expiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1000) : null this.state = { isAuthenticated: true, accessToken: token, refreshToken: null, expiresAt, } this.client.setAccessToken(token) this.saveToStorage() this.notifyStateChange() } /** * Clear authentication state */ clearAuth(): void { if (this.refreshTimeout) { clearTimeout(this.refreshTimeout) } this.state = { isAuthenticated: false, accessToken: null, refreshToken: null, expiresAt: null, } this.client.clearAccessToken() this.clearStorage() this.notifyStateChange() } /** * Get current authentication state */ getState(): AuthState { return { ...this.state } } /** * Check if currently authenticated */ isAuthenticated(): boolean { if (!this.state.isAuthenticated || !this.state.accessToken) { return false } if (this.state.expiresAt && this.state.expiresAt <= new Date()) { return false } return true } /** * Get the authorization URL for OAuth flow */ getAuthorizationUrl(redirectUri: string, scope?: string, state?: string): string { const params = new URLSearchParams({ client_id: this.clientId, redirect_uri: redirectUri, response_type: 'code', ...(scope && { scope }), ...(state && { state }), }) // This would be configured based on the API endpoint return `/oauth/authorize?${params.toString()}` } /** * Destroy the auth provider */ destroy(): void { if (this.refreshTimeout) { clearTimeout(this.refreshTimeout) } } }