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>
612 lines
17 KiB
TypeScript
612 lines
17 KiB
TypeScript
/**
|
|
* BreakPilot Compliance SDK - Embed Script
|
|
*
|
|
* Usage:
|
|
* <script src="https://cdn.breakpilot.app/sdk/v1/compliance.min.js"></script>
|
|
* <script>
|
|
* BreakPilotSDK.init({
|
|
* apiEndpoint: 'https://compliance.example.com/api/v1',
|
|
* apiKey: 'pk_live_xxx',
|
|
* autoInjectBanner: true,
|
|
* bannerConfig: { position: 'bottom', theme: 'dark', language: 'de' }
|
|
* });
|
|
* </script>
|
|
*/
|
|
|
|
import { ComplianceClient } from '@breakpilot/compliance-sdk-core'
|
|
import type {
|
|
ConsentPurpose,
|
|
CookieBannerPosition,
|
|
CookieBannerTheme,
|
|
DSRRequestType,
|
|
} from '@breakpilot/compliance-sdk-types'
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
export interface BreakPilotSDKConfig {
|
|
apiEndpoint: string
|
|
apiKey: string
|
|
tenantId?: string
|
|
autoInjectBanner?: boolean
|
|
bannerConfig?: BannerConfig
|
|
onConsentChange?: (consents: Record<ConsentPurpose, boolean>) => void
|
|
onReady?: () => void
|
|
onError?: (error: Error) => void
|
|
debug?: boolean
|
|
}
|
|
|
|
export interface BannerConfig {
|
|
position?: CookieBannerPosition
|
|
theme?: CookieBannerTheme
|
|
language?: 'de' | 'en'
|
|
privacyPolicyUrl?: string
|
|
imprintUrl?: string
|
|
texts?: {
|
|
title?: string
|
|
description?: string
|
|
acceptAll?: string
|
|
rejectAll?: string
|
|
settings?: string
|
|
save?: string
|
|
}
|
|
customColors?: {
|
|
background?: string
|
|
text?: string
|
|
primary?: string
|
|
secondary?: string
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Internal State
|
|
// ============================================================================
|
|
|
|
let _client: ComplianceClient | null = null
|
|
let _config: BreakPilotSDKConfig | null = null
|
|
let _consents: Record<ConsentPurpose, boolean> = {
|
|
ESSENTIAL: true,
|
|
FUNCTIONAL: false,
|
|
ANALYTICS: false,
|
|
MARKETING: false,
|
|
PERSONALIZATION: false,
|
|
THIRD_PARTY: false,
|
|
}
|
|
let _bannerElement: HTMLElement | null = null
|
|
let _isInitialized = false
|
|
|
|
// ============================================================================
|
|
// Translations
|
|
// ============================================================================
|
|
|
|
const TRANSLATIONS = {
|
|
de: {
|
|
title: 'Cookie-Einwilligung',
|
|
description:
|
|
'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.',
|
|
acceptAll: 'Alle akzeptieren',
|
|
rejectAll: 'Nur notwendige',
|
|
settings: 'Einstellungen',
|
|
save: 'Speichern',
|
|
privacy: 'Datenschutz',
|
|
imprint: 'Impressum',
|
|
categories: {
|
|
ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' },
|
|
FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' },
|
|
ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' },
|
|
MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' },
|
|
PERSONALIZATION: { name: 'Personalisierung', description: 'Angepasste Inhalte' },
|
|
THIRD_PARTY: { name: 'Drittanbieter', description: 'Externe Dienste' },
|
|
},
|
|
},
|
|
en: {
|
|
title: 'Cookie Consent',
|
|
description:
|
|
'We use cookies to improve your experience. For more information, please see our privacy policy.',
|
|
acceptAll: 'Accept All',
|
|
rejectAll: 'Reject Non-Essential',
|
|
settings: 'Settings',
|
|
save: 'Save',
|
|
privacy: 'Privacy Policy',
|
|
imprint: 'Imprint',
|
|
categories: {
|
|
ESSENTIAL: { name: 'Essential', description: 'Required for basic functionality' },
|
|
FUNCTIONAL: { name: 'Functional', description: 'Enhanced features' },
|
|
ANALYTICS: { name: 'Analytics', description: 'Usage statistics' },
|
|
MARKETING: { name: 'Marketing', description: 'Personalized advertising' },
|
|
PERSONALIZATION: { name: 'Personalization', description: 'Customized content' },
|
|
THIRD_PARTY: { name: 'Third Party', description: 'External services' },
|
|
},
|
|
},
|
|
}
|
|
|
|
// ============================================================================
|
|
// Utility Functions
|
|
// ============================================================================
|
|
|
|
function log(message: string, ...args: unknown[]): void {
|
|
if (_config?.debug) {
|
|
console.log(`[BreakPilotSDK] ${message}`, ...args)
|
|
}
|
|
}
|
|
|
|
function getStoredConsents(): Record<ConsentPurpose, boolean> | null {
|
|
try {
|
|
const stored = localStorage.getItem('breakpilot_consents')
|
|
return stored ? JSON.parse(stored) : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function storeConsents(consents: Record<ConsentPurpose, boolean>): void {
|
|
try {
|
|
localStorage.setItem('breakpilot_consents', JSON.stringify(consents))
|
|
localStorage.setItem('breakpilot_consents_timestamp', Date.now().toString())
|
|
} catch {
|
|
log('Failed to store consents')
|
|
}
|
|
}
|
|
|
|
function createElement<K extends keyof HTMLElementTagNameMap>(
|
|
tag: K,
|
|
styles: Partial<CSSStyleDeclaration> = {},
|
|
attributes: Record<string, string> = {}
|
|
): HTMLElementTagNameMap[K] {
|
|
const el = document.createElement(tag)
|
|
Object.assign(el.style, styles)
|
|
Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value))
|
|
return el
|
|
}
|
|
|
|
// ============================================================================
|
|
// Banner Implementation
|
|
// ============================================================================
|
|
|
|
function createBanner(): HTMLElement {
|
|
const config = _config!
|
|
const bannerConfig = config.bannerConfig || {}
|
|
const lang = bannerConfig.language || 'de'
|
|
const t = TRANSLATIONS[lang]
|
|
const position = bannerConfig.position || 'BOTTOM'
|
|
const theme = bannerConfig.theme || 'LIGHT'
|
|
|
|
const isDark = theme === 'DARK'
|
|
const bgColor = bannerConfig.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff')
|
|
const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a')
|
|
const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a')
|
|
const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a')
|
|
|
|
// Container
|
|
const container = createElement(
|
|
'div',
|
|
{
|
|
position: 'fixed',
|
|
zIndex: '99999',
|
|
left: position === 'CENTER' ? '50%' : '0',
|
|
right: position === 'CENTER' ? 'auto' : '0',
|
|
top: position === 'TOP' ? '0' : position === 'CENTER' ? '50%' : 'auto',
|
|
bottom: position === 'BOTTOM' ? '0' : 'auto',
|
|
transform: position === 'CENTER' ? 'translate(-50%, -50%)' : 'none',
|
|
maxWidth: position === 'CENTER' ? '500px' : 'none',
|
|
backgroundColor: bgColor,
|
|
color: textColor,
|
|
padding: '20px',
|
|
boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)',
|
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
fontSize: '14px',
|
|
lineHeight: '1.5',
|
|
},
|
|
{ id: 'breakpilot-consent-banner', role: 'dialog', 'aria-label': t.title }
|
|
)
|
|
|
|
// Title
|
|
const title = createElement('h3', {
|
|
margin: '0 0 10px',
|
|
fontSize: '18px',
|
|
fontWeight: '600',
|
|
})
|
|
title.textContent = bannerConfig.texts?.title || t.title
|
|
|
|
// Description
|
|
const description = createElement('p', {
|
|
margin: '0 0 15px',
|
|
opacity: '0.8',
|
|
})
|
|
description.textContent = bannerConfig.texts?.description || t.description
|
|
|
|
// Buttons container
|
|
const buttonsContainer = createElement('div', {
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: '10px',
|
|
alignItems: 'center',
|
|
})
|
|
|
|
// Accept All button
|
|
const acceptBtn = createElement(
|
|
'button',
|
|
{
|
|
padding: '10px 20px',
|
|
borderRadius: '4px',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
fontWeight: '500',
|
|
backgroundColor: primaryColor,
|
|
color: isDark ? '#1a1a1a' : '#ffffff',
|
|
},
|
|
{ type: 'button' }
|
|
)
|
|
acceptBtn.textContent = bannerConfig.texts?.acceptAll || t.acceptAll
|
|
acceptBtn.onclick = () => handleAcceptAll()
|
|
|
|
// Reject button
|
|
const rejectBtn = createElement(
|
|
'button',
|
|
{
|
|
padding: '10px 20px',
|
|
borderRadius: '4px',
|
|
backgroundColor: 'transparent',
|
|
border: `1px solid ${secondaryColor}`,
|
|
color: textColor,
|
|
cursor: 'pointer',
|
|
fontWeight: '500',
|
|
},
|
|
{ type: 'button' }
|
|
)
|
|
rejectBtn.textContent = bannerConfig.texts?.rejectAll || t.rejectAll
|
|
rejectBtn.onclick = () => handleRejectAll()
|
|
|
|
// Settings button
|
|
const settingsBtn = createElement(
|
|
'button',
|
|
{
|
|
padding: '10px 20px',
|
|
borderRadius: '4px',
|
|
backgroundColor: 'transparent',
|
|
border: `1px solid ${secondaryColor}`,
|
|
color: textColor,
|
|
cursor: 'pointer',
|
|
fontWeight: '500',
|
|
},
|
|
{ type: 'button' }
|
|
)
|
|
settingsBtn.textContent = bannerConfig.texts?.settings || t.settings
|
|
settingsBtn.onclick = () => showSettingsPanel()
|
|
|
|
// Links container
|
|
const linksContainer = createElement('div', {
|
|
marginLeft: 'auto',
|
|
fontSize: '12px',
|
|
})
|
|
|
|
const privacyLink = createElement('a', {
|
|
marginRight: '15px',
|
|
color: textColor,
|
|
textDecoration: 'none',
|
|
})
|
|
privacyLink.href = bannerConfig.privacyPolicyUrl || '/privacy'
|
|
privacyLink.textContent = t.privacy
|
|
|
|
const imprintLink = createElement('a', {
|
|
color: textColor,
|
|
textDecoration: 'none',
|
|
})
|
|
imprintLink.href = bannerConfig.imprintUrl || '/imprint'
|
|
imprintLink.textContent = t.imprint
|
|
|
|
// Assemble
|
|
linksContainer.appendChild(privacyLink)
|
|
linksContainer.appendChild(imprintLink)
|
|
|
|
buttonsContainer.appendChild(acceptBtn)
|
|
buttonsContainer.appendChild(rejectBtn)
|
|
buttonsContainer.appendChild(settingsBtn)
|
|
buttonsContainer.appendChild(linksContainer)
|
|
|
|
container.appendChild(title)
|
|
container.appendChild(description)
|
|
container.appendChild(buttonsContainer)
|
|
|
|
return container
|
|
}
|
|
|
|
function showSettingsPanel(): void {
|
|
if (!_bannerElement) return
|
|
|
|
const config = _config!
|
|
const bannerConfig = config.bannerConfig || {}
|
|
const lang = bannerConfig.language || 'de'
|
|
const t = TRANSLATIONS[lang]
|
|
const theme = bannerConfig.theme || 'LIGHT'
|
|
const isDark = theme === 'DARK'
|
|
const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a')
|
|
|
|
// Clear banner content
|
|
_bannerElement.innerHTML = ''
|
|
|
|
// Title
|
|
const title = createElement('h3', {
|
|
margin: '0 0 15px',
|
|
fontSize: '18px',
|
|
fontWeight: '600',
|
|
})
|
|
title.textContent = t.settings
|
|
|
|
_bannerElement.appendChild(title)
|
|
|
|
// Categories
|
|
const categories: ConsentPurpose[] = [
|
|
'ESSENTIAL',
|
|
'FUNCTIONAL',
|
|
'ANALYTICS',
|
|
'MARKETING',
|
|
]
|
|
|
|
categories.forEach(category => {
|
|
const catInfo = t.categories[category]
|
|
const row = createElement('div', {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: '10px 0',
|
|
borderBottom: `1px solid ${isDark ? '#333' : '#eee'}`,
|
|
})
|
|
|
|
const labelContainer = createElement('div', {})
|
|
const labelName = createElement('div', { fontWeight: '500' })
|
|
labelName.textContent = catInfo.name
|
|
const labelDesc = createElement('div', { fontSize: '12px', opacity: '0.7' })
|
|
labelDesc.textContent = catInfo.description
|
|
|
|
labelContainer.appendChild(labelName)
|
|
labelContainer.appendChild(labelDesc)
|
|
|
|
const checkbox = createElement(
|
|
'input',
|
|
{
|
|
width: '20px',
|
|
height: '20px',
|
|
cursor: category === 'ESSENTIAL' ? 'not-allowed' : 'pointer',
|
|
},
|
|
{
|
|
type: 'checkbox',
|
|
'data-category': category,
|
|
}
|
|
)
|
|
checkbox.checked = _consents[category]
|
|
checkbox.disabled = category === 'ESSENTIAL'
|
|
checkbox.onchange = () => {
|
|
if (category !== 'ESSENTIAL') {
|
|
_consents[category] = checkbox.checked
|
|
}
|
|
}
|
|
|
|
row.appendChild(labelContainer)
|
|
row.appendChild(checkbox)
|
|
_bannerElement!.appendChild(row)
|
|
})
|
|
|
|
// Buttons
|
|
const buttonsContainer = createElement('div', {
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: '10px',
|
|
marginTop: '15px',
|
|
})
|
|
|
|
const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a')
|
|
const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a')
|
|
|
|
const saveBtn = createElement(
|
|
'button',
|
|
{
|
|
padding: '10px 20px',
|
|
borderRadius: '4px',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
fontWeight: '500',
|
|
backgroundColor: primaryColor,
|
|
color: isDark ? '#1a1a1a' : '#ffffff',
|
|
},
|
|
{ type: 'button' }
|
|
)
|
|
saveBtn.textContent = bannerConfig.texts?.save || t.save
|
|
saveBtn.onclick = () => handleSaveSettings()
|
|
|
|
const backBtn = createElement(
|
|
'button',
|
|
{
|
|
padding: '10px 20px',
|
|
borderRadius: '4px',
|
|
backgroundColor: 'transparent',
|
|
border: `1px solid ${secondaryColor}`,
|
|
color: textColor,
|
|
cursor: 'pointer',
|
|
fontWeight: '500',
|
|
},
|
|
{ type: 'button' }
|
|
)
|
|
backBtn.textContent = 'Zurück'
|
|
backBtn.onclick = () => showMainBanner()
|
|
|
|
buttonsContainer.appendChild(saveBtn)
|
|
buttonsContainer.appendChild(backBtn)
|
|
_bannerElement.appendChild(buttonsContainer)
|
|
}
|
|
|
|
function showMainBanner(): void {
|
|
if (_bannerElement) {
|
|
_bannerElement.remove()
|
|
}
|
|
_bannerElement = createBanner()
|
|
document.body.appendChild(_bannerElement)
|
|
}
|
|
|
|
function hideBanner(): void {
|
|
if (_bannerElement) {
|
|
_bannerElement.remove()
|
|
_bannerElement = null
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Consent Handlers
|
|
// ============================================================================
|
|
|
|
function handleAcceptAll(): void {
|
|
_consents = {
|
|
ESSENTIAL: true,
|
|
FUNCTIONAL: true,
|
|
ANALYTICS: true,
|
|
MARKETING: true,
|
|
PERSONALIZATION: true,
|
|
THIRD_PARTY: true,
|
|
}
|
|
applyConsents()
|
|
}
|
|
|
|
function handleRejectAll(): void {
|
|
_consents = {
|
|
ESSENTIAL: true,
|
|
FUNCTIONAL: false,
|
|
ANALYTICS: false,
|
|
MARKETING: false,
|
|
PERSONALIZATION: false,
|
|
THIRD_PARTY: false,
|
|
}
|
|
applyConsents()
|
|
}
|
|
|
|
function handleSaveSettings(): void {
|
|
applyConsents()
|
|
}
|
|
|
|
function applyConsents(): void {
|
|
storeConsents(_consents)
|
|
hideBanner()
|
|
_config?.onConsentChange?.(_consents)
|
|
log('Consents applied:', _consents)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Public API
|
|
// ============================================================================
|
|
|
|
function init(config: BreakPilotSDKConfig): void {
|
|
if (_isInitialized) {
|
|
log('SDK already initialized')
|
|
return
|
|
}
|
|
|
|
_config = config
|
|
log('Initializing with config:', config)
|
|
|
|
try {
|
|
_client = new ComplianceClient({
|
|
apiEndpoint: config.apiEndpoint,
|
|
apiKey: config.apiKey,
|
|
tenantId: config.tenantId,
|
|
})
|
|
|
|
// Check for stored consents
|
|
const storedConsents = getStoredConsents()
|
|
if (storedConsents) {
|
|
_consents = storedConsents
|
|
log('Loaded stored consents:', _consents)
|
|
config.onConsentChange?.(storedConsents)
|
|
} else if (config.autoInjectBanner !== false) {
|
|
// Show banner if no stored consents
|
|
showMainBanner()
|
|
}
|
|
|
|
_isInitialized = true
|
|
config.onReady?.()
|
|
log('SDK initialized successfully')
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error(String(error))
|
|
config.onError?.(err)
|
|
log('SDK initialization failed:', err)
|
|
}
|
|
}
|
|
|
|
function getConsents(): Record<ConsentPurpose, boolean> {
|
|
return { ..._consents }
|
|
}
|
|
|
|
function updateConsent(purpose: ConsentPurpose, granted: boolean): void {
|
|
if (purpose === 'ESSENTIAL') {
|
|
log('Cannot change essential consent')
|
|
return
|
|
}
|
|
_consents[purpose] = granted
|
|
storeConsents(_consents)
|
|
_config?.onConsentChange?.(_consents)
|
|
log('Consent updated:', purpose, granted)
|
|
}
|
|
|
|
function showBanner(): void {
|
|
showMainBanner()
|
|
}
|
|
|
|
function hasConsent(purpose: ConsentPurpose): boolean {
|
|
return _consents[purpose]
|
|
}
|
|
|
|
function getClient(): ComplianceClient | null {
|
|
return _client
|
|
}
|
|
|
|
async function submitDSR(
|
|
type: DSRRequestType,
|
|
email: string,
|
|
name: string
|
|
): Promise<{ success: boolean; requestId?: string; error?: string }> {
|
|
if (!_client) {
|
|
return { success: false, error: 'SDK not initialized' }
|
|
}
|
|
|
|
try {
|
|
// This would call the DSR endpoint
|
|
log('Submitting DSR:', { type, email, name })
|
|
// In a real implementation, this would use the client to submit
|
|
return { success: true, requestId: `dsr_${Date.now()}` }
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
return { success: false, error: message }
|
|
}
|
|
}
|
|
|
|
function destroy(): void {
|
|
hideBanner()
|
|
_client = null
|
|
_config = null
|
|
_isInitialized = false
|
|
log('SDK destroyed')
|
|
}
|
|
|
|
// ============================================================================
|
|
// Export for IIFE
|
|
// ============================================================================
|
|
|
|
export const BreakPilotSDK = {
|
|
init,
|
|
getConsents,
|
|
updateConsent,
|
|
showBanner,
|
|
hideBanner,
|
|
hasConsent,
|
|
getClient,
|
|
submitDSR,
|
|
destroy,
|
|
version: '0.0.1',
|
|
}
|
|
|
|
// Auto-attach to window for script tag usage
|
|
if (typeof window !== 'undefined') {
|
|
;(window as unknown as Record<string, unknown>).BreakPilotSDK = BreakPilotSDK
|
|
}
|
|
|
|
export default BreakPilotSDK
|