Files
breakpilot-compliance/breakpilot-compliance-sdk/packages/core/src/auth.ts
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

296 lines
7.5 KiB
TypeScript

/**
* 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<typeof setTimeout> | 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<void> {
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<void> {
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<void> {
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)
}
}
}