Phase 4 continuation. All touched files now under the file-size cap, and drive-by fixes unblock the types/core/react/vanilla builds which were broken at baseline. Splits - packages/types/src/state 505 -> 31 LOC barrel + state-flow/-assessment/-core - packages/core/src/client 521 -> 395 LOC + client-http 187 LOC (HTTP transport) - packages/react/src/provider 539 -> 460 LOC + provider-context 101 LOC - packages/vanilla/src/embed 611 -> 290 LOC + embed-banner 321 + embed-translations 78 Drive-by fixes (pre-existing typecheck/build failures) - types/rag.ts: rename colliding LegalDocument export to RagLegalDocument (the `export *` chain in index.ts was ambiguous; two consumers updated - core/modules/rag.ts drops unused import, vue/composables/useRAG.ts switches to the renamed symbol). - core/modules/rag.ts: wrap client searchRAG response to add the missing `query` field so the declared SearchResponse return type is satisfied. - react/provider.tsx: re-export useCompliance so ComplianceDashboard / ConsentBanner / DSRPortal legacy `from '../provider'` imports resolve. - vanilla/embed.ts + web-components/base.ts: default tenantId to '' so ComplianceClient construction typechecks. - vanilla/web-components/consent-banner.ts: tighten categories literal to `as const` so t.categories indexing narrows correctly. Verification: packages/types + core + react + vanilla all `pnpm build` clean with DTS emission. consent-sdk unaffected (still green). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
290 lines
7.4 KiB
TypeScript
290 lines
7.4 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,
|
|
DSRRequestType,
|
|
} from '@breakpilot/compliance-sdk-types'
|
|
import {
|
|
createBannerElement,
|
|
renderSettingsPanel,
|
|
type BannerConfig,
|
|
type BannerContext,
|
|
} from './embed-banner'
|
|
|
|
// ============================================================================
|
|
// 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 type { BannerConfig } from './embed-banner'
|
|
|
|
// ============================================================================
|
|
// 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
|
|
|
|
// ============================================================================
|
|
// 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')
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Banner Orchestration
|
|
// ============================================================================
|
|
|
|
function buildBannerContext(): BannerContext {
|
|
return {
|
|
config: _config?.bannerConfig || {},
|
|
consents: _consents,
|
|
callbacks: {
|
|
onAcceptAll: () => handleAcceptAll(),
|
|
onRejectAll: () => handleRejectAll(),
|
|
onShowSettings: () => showSettingsPanel(),
|
|
onToggleCategory: (category, granted) => {
|
|
_consents[category] = granted
|
|
},
|
|
onSaveSettings: () => handleSaveSettings(),
|
|
onBack: () => showMainBanner(),
|
|
},
|
|
}
|
|
}
|
|
|
|
function showMainBanner(): void {
|
|
if (_bannerElement) {
|
|
_bannerElement.remove()
|
|
}
|
|
_bannerElement = createBannerElement(buildBannerContext())
|
|
document.body.appendChild(_bannerElement)
|
|
}
|
|
|
|
function showSettingsPanel(): void {
|
|
if (!_bannerElement) return
|
|
renderSettingsPanel(_bannerElement, buildBannerContext())
|
|
}
|
|
|
|
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
|