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>
This commit is contained in:
295
breakpilot-compliance-sdk/packages/core/src/auth.ts
Normal file
295
breakpilot-compliance-sdk/packages/core/src/auth.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user