Files
breakpilot-compliance/breakpilot-compliance-sdk/packages/vanilla/src/embed.ts
Sharang Parnerkar 5cb91e88d2 refactor(compliance-sdk): split client/provider/embed/state under 500 LOC
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>
2026-04-11 22:39:47 +02:00

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