Files
breakpilot-compliance/admin-compliance/app/sdk/document-generator/contextBridge.ts
Benjamin Admin eca0855216 feat: Sidebar-Links fuer Developer Portal + SDK Dokumentation
Externe Links oeffnen in neuem Tab mit Icon-Indikator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:24:39 +01:00

394 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Template-Spec v1 — Context Bridge
*
* Maps a structured TemplateContext (canonical entities: PROVIDER, CUSTOMER, etc.)
* to the flat {{PLACEHOLDER}} map used by legacy templates.
*
* Flow: TemplateContext → contextToPlaceholders() → Record<string, string>
* → string.replace({{KEY}}, value) in renderer
*/
// =============================================================================
// Types
// =============================================================================
export interface ProviderCtx {
LEGAL_NAME: string
LEGAL_FORM?: string
ADDRESS_LINE: string
POSTAL_CODE: string
CITY: string
COUNTRY: string
EMAIL: string
PHONE?: string
WEBSITE_URL?: string
CEO_NAME?: string
REGISTER_COURT?: string
REGISTER_NUMBER?: string
VAT_ID?: string
}
export interface CustomerCtx {
LEGAL_NAME: string
ADDRESS_LINE?: string
POSTAL_CODE?: string
CITY?: string
COUNTRY?: string
CONTACT_NAME?: string
EMAIL: string
IS_CONSUMER: boolean
IS_BUSINESS: boolean
}
export interface ServiceCtx {
NAME: string
DESCRIPTION: string
MODEL: 'SaaS' | 'PaaS' | 'IaaS' | 'OnPrem' | 'Hybrid' | ''
TIER: string
DATA_LOCATION: string
EXPORT_FORMATS?: string[]
EXPORT_WINDOW_DAYS?: number
MIN_TERM_MONTHS?: number
TERMINATION_NOTICE_DAYS?: number
}
export interface LegalCtx {
GOVERNING_LAW: string
JURISDICTION_CITY: string
VERSION_DATE: string // YYYY-MM-DD
EFFECTIVE_DATE: string // YYYY-MM-DD
LANG: 'de' | 'en'
}
export interface PrivacyCtx {
CONTACT_EMAIL: string
DPO_NAME: string
DPO_EMAIL: string
SUPERVISORY_AUTHORITY_NAME?: string
SUPERVISORY_AUTHORITY_ADDRESS?: string
PRIVACY_POLICY_URL: string
COOKIE_POLICY_URL?: string
ANALYTICS_RETENTION_MONTHS?: number
DATA_TRANSFER_THIRD_COUNTRIES?: string
}
export interface SLACtx {
AVAILABILITY_PERCENT: number | ''
MAINTENANCE_NOTICE_HOURS: number | ''
SUPPORT_EMAIL: string
SUPPORT_PHONE?: string
SUPPORT_HOURS: string
RESPONSE_CRITICAL_H: number | ''
RESOLUTION_CRITICAL_H: number | ''
RESPONSE_HIGH_H: number | ''
RESOLUTION_HIGH_H: number | ''
RESPONSE_MEDIUM_H: number | ''
RESOLUTION_MEDIUM_H: number | ''
RESPONSE_LOW_H: number | ''
}
export interface PaymentsCtx {
MONTHLY_FEE_EUR: number | ''
PAYMENT_DUE_DAY: number | ''
PAYMENT_METHOD: string
PAYMENT_DAYS: number | ''
}
export interface SecurityCtx {
INCIDENT_NOTICE_HOURS: number | ''
LOG_RETENTION_DAYS: number | ''
SECURITY_LOG_RETENTION_DAYS: number | ''
}
export interface NDACtx {
PURPOSE: string
DURATION_YEARS: number | ''
PENALTY_AMOUNT_EUR: number | null | ''
}
export interface ConsentCtx {
WEBSITE_NAME: string
ANALYTICS_TOOLS: string | null
MARKETING_PARTNERS: string | null
}
export interface TemplateContext {
PROVIDER: ProviderCtx
CUSTOMER: CustomerCtx
SERVICE: ServiceCtx
LEGAL: LegalCtx
PRIVACY: PrivacyCtx
SLA: SLACtx
PAYMENTS: PaymentsCtx
SECURITY: SecurityCtx
NDA: NDACtx
CONSENT: ConsentCtx
}
export interface ComputedFlags {
IS_B2C: boolean
IS_B2B: boolean
SERVICE_IS_SAAS: boolean
SERVICE_IS_HYBRID: boolean
HAS_PENALTY: boolean
HAS_ANALYTICS: boolean
HAS_MARKETING: boolean
}
// =============================================================================
// Empty default context
// =============================================================================
export const EMPTY_CONTEXT: TemplateContext = {
PROVIDER: {
LEGAL_NAME: '', LEGAL_FORM: '', ADDRESS_LINE: '', POSTAL_CODE: '', CITY: '',
COUNTRY: 'DE', EMAIL: '', PHONE: '', WEBSITE_URL: '',
CEO_NAME: '', REGISTER_COURT: '', REGISTER_NUMBER: '', VAT_ID: '',
},
CUSTOMER: {
LEGAL_NAME: '', ADDRESS_LINE: '', POSTAL_CODE: '', CITY: '', COUNTRY: 'DE',
CONTACT_NAME: '', EMAIL: '', IS_CONSUMER: false, IS_BUSINESS: true,
},
SERVICE: {
NAME: '', DESCRIPTION: '', MODEL: 'SaaS', TIER: '', DATA_LOCATION: 'Deutschland',
EXPORT_FORMATS: [], EXPORT_WINDOW_DAYS: 30, MIN_TERM_MONTHS: 12,
TERMINATION_NOTICE_DAYS: 30,
},
LEGAL: {
GOVERNING_LAW: 'Deutschland', JURISDICTION_CITY: '', VERSION_DATE: '', EFFECTIVE_DATE: '',
LANG: 'de',
},
PRIVACY: {
CONTACT_EMAIL: '', DPO_NAME: '', DPO_EMAIL: '',
SUPERVISORY_AUTHORITY_NAME: '', SUPERVISORY_AUTHORITY_ADDRESS: '',
PRIVACY_POLICY_URL: '', COOKIE_POLICY_URL: '',
ANALYTICS_RETENTION_MONTHS: 13, DATA_TRANSFER_THIRD_COUNTRIES: 'nicht statt',
},
SLA: {
AVAILABILITY_PERCENT: 99.5, MAINTENANCE_NOTICE_HOURS: 72,
SUPPORT_EMAIL: '', SUPPORT_PHONE: '', SUPPORT_HOURS: 'MoFr 09:0018:00 CET',
RESPONSE_CRITICAL_H: 2, RESOLUTION_CRITICAL_H: 8,
RESPONSE_HIGH_H: 4, RESOLUTION_HIGH_H: 24,
RESPONSE_MEDIUM_H: 24, RESOLUTION_MEDIUM_H: 120,
RESPONSE_LOW_H: 72,
},
PAYMENTS: {
MONTHLY_FEE_EUR: '', PAYMENT_DUE_DAY: 1, PAYMENT_METHOD: 'Rechnung', PAYMENT_DAYS: 14,
},
SECURITY: {
INCIDENT_NOTICE_HOURS: 24, LOG_RETENTION_DAYS: 7, SECURITY_LOG_RETENTION_DAYS: 30,
},
NDA: { PURPOSE: '', DURATION_YEARS: 5, PENALTY_AMOUNT_EUR: null },
CONSENT: { WEBSITE_NAME: '', ANALYTICS_TOOLS: null, MARKETING_PARTNERS: null },
}
// =============================================================================
// Bridge: context → flat placeholder map
// =============================================================================
function str(v: unknown): string {
if (v === null || v === undefined || v === '') return ''
if (Array.isArray(v)) return v.join(', ')
return String(v)
}
/** Compute compound address from PROVIDER fields */
function providerAddress(p: ProviderCtx): string {
const parts = [p.ADDRESS_LINE, [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' ')].filter(Boolean)
return parts.join(', ')
}
/**
* Maps a TemplateContext to a flat Record<string, string> of placeholder values.
* Keys are the {{PLACEHOLDER}} strings used in templates.
*/
export function contextToPlaceholders(ctx: TemplateContext): Record<string, string> {
const p = ctx.PROVIDER
const c = ctx.CUSTOMER
const s = ctx.SERVICE
const l = ctx.LEGAL
const prv = ctx.PRIVACY
const sla = ctx.SLA
const pay = ctx.PAYMENTS
const sec = ctx.SECURITY
const nda = ctx.NDA
const con = ctx.CONSENT
const address = providerAddress(p)
return {
// --- PROVIDER ---
'{{COMPANY_NAME}}': str(p.LEGAL_NAME),
'{{PROVIDER_NAME}}': str(p.LEGAL_NAME),
'{{DISCLOSING_PARTY}}': str(p.LEGAL_NAME), // NDA: provider is disclosing party
'{{SERVICE_PROVIDER}}': str(p.LEGAL_NAME), // SLA
'{{COMPANY_ADDRESS}}': address,
'{{PROVIDER_ADDRESS}}': address,
'{{CONTACT_EMAIL}}': str(p.EMAIL),
'{{PROVIDER_EMAIL}}': str(p.EMAIL),
'{{ABUSE_EMAIL}}': str(p.EMAIL), // AUP abuse contact
'{{REPORT_EMAIL}}': prv.CONTACT_EMAIL ? str(prv.CONTACT_EMAIL) : str(p.EMAIL), // community moderation
// --- CUSTOMER ---
'{{CUSTOMER_NAME}}': str(c.LEGAL_NAME),
'{{RECEIVING_PARTY}}': str(c.LEGAL_NAME), // NDA
'{{CUSTOMER}}': str(c.LEGAL_NAME), // SLA
// --- SERVICE ---
'{{SERVICE_NAME}}': str(s.NAME),
'{{PLATFORM_NAME}}': str(s.NAME), // community / copyright
'{{SERVICE_DESCRIPTION}}': str(s.DESCRIPTION),
'{{SERVICE_MODEL}}': str(s.MODEL),
'{{SERVICE_TIER}}': str(s.TIER),
'{{DATA_LOCATION}}': str(s.DATA_LOCATION),
'{{EXPORT_FORMATS}}': Array.isArray(s.EXPORT_FORMATS) ? s.EXPORT_FORMATS.join(', ') : '',
'{{EXPORT_WINDOW_DAYS}}': str(s.EXPORT_WINDOW_DAYS),
'{{MIN_TERM_MONTHS}}': str(s.MIN_TERM_MONTHS),
'{{TERMINATION_NOTICE_DAYS}}': str(s.TERMINATION_NOTICE_DAYS),
// --- LEGAL ---
'{{GOVERNING_LAW}}': str(l.GOVERNING_LAW),
'{{JURISDICTION_CITY}}': str(l.JURISDICTION_CITY),
'{{VERSION_DATE}}': str(l.VERSION_DATE),
'{{EFFECTIVE_DATE}}': str(l.EFFECTIVE_DATE),
'{{START_DATE}}': str(l.EFFECTIVE_DATE), // cloud contract start date
// --- PRIVACY ---
'{{PRIVACY_CONTACT_EMAIL}}': str(prv.CONTACT_EMAIL),
'{{DPO_NAME}}': str(prv.DPO_NAME),
'{{DPO_EMAIL}}': str(prv.DPO_EMAIL),
'{{COPYRIGHT_CONTACT_NAME}}': str(prv.DPO_NAME), // copyright policy
'{{COPYRIGHT_EMAIL}}': str(prv.DPO_EMAIL),
'{{PRIVACY_POLICY_URL}}': str(prv.PRIVACY_POLICY_URL),
'{{COOKIE_POLICY_URL}}': str(prv.COOKIE_POLICY_URL),
'{{ANALYTICS_RETENTION_MONTHS}}': str(prv.ANALYTICS_RETENTION_MONTHS),
'{{DATA_TRANSFER_THIRD_COUNTRIES}}': str(prv.DATA_TRANSFER_THIRD_COUNTRIES),
// --- SLA ---
'{{AVAILABILITY_PERCENT}}': str(sla.AVAILABILITY_PERCENT),
'{{MAINTENANCE_NOTICE_HOURS}}': str(sla.MAINTENANCE_NOTICE_HOURS),
'{{SUPPORT_EMAIL}}': str(sla.SUPPORT_EMAIL),
'{{SUPPORT_PHONE}}': str(sla.SUPPORT_PHONE),
'{{SUPPORT_HOURS}}': str(sla.SUPPORT_HOURS),
'{{RESPONSE_CRITICAL_H}}': str(sla.RESPONSE_CRITICAL_H),
'{{RESOLUTION_CRITICAL_H}}': str(sla.RESOLUTION_CRITICAL_H),
'{{RESPONSE_HIGH_H}}': str(sla.RESPONSE_HIGH_H),
'{{RESOLUTION_HIGH_H}}': str(sla.RESOLUTION_HIGH_H),
'{{RESPONSE_MEDIUM_H}}': str(sla.RESPONSE_MEDIUM_H),
'{{RESOLUTION_MEDIUM_H}}': str(sla.RESOLUTION_MEDIUM_H),
'{{RESPONSE_LOW_H}}': str(sla.RESPONSE_LOW_H),
// --- PAYMENTS ---
'{{MONTHLY_FEE}}': str(pay.MONTHLY_FEE_EUR),
'{{PAYMENT_DUE_DAY}}': str(pay.PAYMENT_DUE_DAY),
'{{PAYMENT_METHOD}}': str(pay.PAYMENT_METHOD),
'{{PAYMENT_DAYS}}': str(pay.PAYMENT_DAYS),
// --- SECURITY ---
'{{INCIDENT_NOTICE_HOURS}}': str(sec.INCIDENT_NOTICE_HOURS),
'{{LOG_RETENTION_DAYS}}': str(sec.LOG_RETENTION_DAYS),
'{{SECURITY_LOG_RETENTION_DAYS}}': str(sec.SECURITY_LOG_RETENTION_DAYS),
// --- NDA ---
'{{PURPOSE}}': str(nda.PURPOSE),
'{{DURATION_YEARS}}': str(nda.DURATION_YEARS),
'{{PENALTY_AMOUNT}}': nda.PENALTY_AMOUNT_EUR !== null ? str(nda.PENALTY_AMOUNT_EUR) : '',
// --- CONSENT ---
'{{WEBSITE_NAME}}': con.WEBSITE_NAME || str(p.WEBSITE_URL),
'{{ANALYTICS_TOOLS}}': str(con.ANALYTICS_TOOLS),
'{{MARKETING_PARTNERS}}': str(con.MARKETING_PARTNERS),
}
}
// =============================================================================
// Computed flags
// =============================================================================
export function computeFlags(ctx: TemplateContext): ComputedFlags {
return {
IS_B2C: ctx.CUSTOMER.IS_CONSUMER,
IS_B2B: ctx.CUSTOMER.IS_BUSINESS,
SERVICE_IS_SAAS: ctx.SERVICE.MODEL === 'SaaS',
SERVICE_IS_HYBRID: ctx.SERVICE.MODEL === 'Hybrid',
HAS_PENALTY: ctx.NDA.PENALTY_AMOUNT_EUR !== null && ctx.NDA.PENALTY_AMOUNT_EUR !== '',
HAS_ANALYTICS: !!ctx.CONSENT.ANALYTICS_TOOLS,
HAS_MARKETING: !!ctx.CONSENT.MARKETING_PARTNERS,
}
}
// =============================================================================
// Section relevance — which sections are useful for a given template
// =============================================================================
/** All placeholder patterns covered by each context section */
const SECTION_COVERS: Record<keyof TemplateContext, string[]> = {
PROVIDER: ['{{COMPANY_NAME}}', '{{PROVIDER_NAME}}', '{{DISCLOSING_PARTY}}', '{{SERVICE_PROVIDER}}', '{{COMPANY_ADDRESS}}', '{{PROVIDER_ADDRESS}}', '{{CONTACT_EMAIL}}', '{{PROVIDER_EMAIL}}', '{{ABUSE_EMAIL}}', '{{REPORT_EMAIL}}'],
CUSTOMER: ['{{CUSTOMER_NAME}}', '{{RECEIVING_PARTY}}', '{{CUSTOMER}}'],
SERVICE: ['{{SERVICE_NAME}}', '{{PLATFORM_NAME}}', '{{SERVICE_DESCRIPTION}}', '{{SERVICE_MODEL}}', '{{SERVICE_TIER}}', '{{DATA_LOCATION}}', '{{EXPORT_FORMATS}}', '{{EXPORT_WINDOW_DAYS}}', '{{MIN_TERM_MONTHS}}', '{{TERMINATION_NOTICE_DAYS}}'],
LEGAL: ['{{GOVERNING_LAW}}', '{{JURISDICTION_CITY}}', '{{VERSION_DATE}}', '{{EFFECTIVE_DATE}}', '{{START_DATE}}'],
PRIVACY: ['{{PRIVACY_CONTACT_EMAIL}}', '{{DPO_NAME}}', '{{DPO_EMAIL}}', '{{COPYRIGHT_CONTACT_NAME}}', '{{COPYRIGHT_EMAIL}}', '{{PRIVACY_POLICY_URL}}', '{{COOKIE_POLICY_URL}}', '{{ANALYTICS_RETENTION_MONTHS}}', '{{DATA_TRANSFER_THIRD_COUNTRIES}}'],
SLA: ['{{AVAILABILITY_PERCENT}}', '{{MAINTENANCE_NOTICE_HOURS}}', '{{SUPPORT_EMAIL}}', '{{SUPPORT_PHONE}}', '{{SUPPORT_HOURS}}', '{{RESPONSE_CRITICAL_H}}', '{{RESOLUTION_CRITICAL_H}}', '{{RESPONSE_HIGH_H}}', '{{RESOLUTION_HIGH_H}}', '{{RESPONSE_MEDIUM_H}}', '{{RESOLUTION_MEDIUM_H}}', '{{RESPONSE_LOW_H}}'],
PAYMENTS: ['{{MONTHLY_FEE}}', '{{PAYMENT_DUE_DAY}}', '{{PAYMENT_METHOD}}', '{{PAYMENT_DAYS}}'],
SECURITY: ['{{INCIDENT_NOTICE_HOURS}}', '{{LOG_RETENTION_DAYS}}', '{{SECURITY_LOG_RETENTION_DAYS}}'],
NDA: ['{{PURPOSE}}', '{{DURATION_YEARS}}', '{{PENALTY_AMOUNT}}'],
CONSENT: ['{{WEBSITE_NAME}}', '{{ANALYTICS_TOOLS}}', '{{MARKETING_PARTNERS}}'],
}
/**
* Returns which context sections are relevant for a given list of template placeholders.
*/
export function getRelevantSections(placeholders: string[]): (keyof TemplateContext)[] {
const pSet = new Set(placeholders)
return (Object.keys(SECTION_COVERS) as (keyof TemplateContext)[]).filter(
(section) => SECTION_COVERS[section].some((ph) => pSet.has(ph))
)
}
/**
* Returns a list of placeholder keys that are NOT covered by the context bridge.
* These need to be filled in manually.
*/
export function getUncoveredPlaceholders(placeholders: string[], ctx: TemplateContext): string[] {
const bridged = contextToPlaceholders(ctx)
return placeholders.filter((ph) => !(ph in bridged))
}
/**
* Strict validation: returns placeholder keys that are required but empty.
* "Required" means: placeholder appears in template AND bridge covers it AND bridge produces empty string.
*/
export function getMissingRequired(placeholders: string[], ctx: TemplateContext): string[] {
const bridged = contextToPlaceholders(ctx)
return placeholders.filter((ph) => ph in bridged && !bridged[ph])
}
// =============================================================================
// Helpers to set nested context values by dot-path
// =============================================================================
/**
* Returns a new TemplateContext with the value at dotPath set.
* e.g. setContextPath(ctx, 'PROVIDER.LEGAL_NAME', 'ACME GmbH')
*/
export function setContextPath(ctx: TemplateContext, dotPath: string, value: unknown): TemplateContext {
const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]]
const key = rest.join('.')
return {
...ctx,
[section]: {
...(ctx[section] as Record<string, unknown>),
[key]: value,
},
}
}
/**
* Get a nested context value by dot-path.
*/
export function getContextPath(ctx: TemplateContext, dotPath: string): unknown {
const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]]
const sectionObj = ctx[section] as Record<string, unknown>
return sectionObj?.[rest.join('.')]
}