import { PDFDocument, rgb, StandardFonts } from 'pdfkit'; import { ReportDataPayload } from '@shieldai/types'; interface PdfContext { reportTitle: string; periodStart: string; periodEnd: string; generatedAt: string; data: ReportDataPayload; reportId: string; } function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); } function getScoreColor(score: number): string { if (score >= 70) return '#22c55e'; if (score >= 40) return '#eab308'; return '#ef4444'; } export class PdfGenerator { async generate(context: PdfContext): Promise { return new Promise((resolve, reject) => { const doc = new PDFDocument({ size: 'A4', margins: { top: 40, bottom: 40, left: 40, right: 40 }, }); const chunks: Buffer[] = []; doc.on('data', (chunk) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); const w = doc.page.width; const h = doc.page.height; // Header doc .rect(0, 0, w, 120) .fill('#1e40af') .fillColor('white') .font(StandardFonts.HelveticaBold) .fontSize(24) .text(context.reportTitle, 40, 30, { align: 'center' }) .fontSize(12) .fillColor('rgba(255,255,255,0.8)') .text(`${formatDate(context.periodStart)} — ${formatDate(context.periodEnd)}`, 40, 70, { align: 'center' }) .text(`Generated ${formatDate(context.generatedAt)}`, 40, 85, { align: 'center' }); let y = 140; // Protection Score Section y = this.drawSectionHeader(doc, 'Protection Score', y); const score = context.data.protectionScore; const scoreColor = getScoreColor(score); doc .fillColor(scoreColor) .fontSize(48) .font(StandardFonts.HelveticaBold) .text(`${score}/100`, 40, y, { align: 'center' }); y += 60; if (context.data.previousProtectionScore !== undefined) { const change = score - context.data.previousProtectionScore; const changeText = change > 0 ? `+${change} from previous period` : `${change} from previous period`; doc .fillColor('#64748b') .fontSize(11) .font(StandardFonts.Helvetica) .text(changeText, 40, y, { align: 'center' }); y += 20; } // Exposure Summary y = this.drawSectionHeader(doc, 'Dark Web Exposure Summary', y); y = this.drawStatGrid(doc, [ { label: 'Critical', value: context.data.exposureSummary.criticalExposures, color: '#ef4444' }, { label: 'Warnings', value: context.data.exposureSummary.warningExposures, color: '#eab308' }, { label: 'New Findings', value: context.data.exposureSummary.newExposures, color: '#2563eb' }, { label: 'Resolved', value: context.data.exposureSummary.resolvedExposures, color: '#22c55e' }, ], y); // Spam Protection y = this.drawSectionHeader(doc, 'Spam Protection', y); y = this.drawStatGrid(doc, [ { label: 'Calls Blocked', value: context.data.spamStats.callsBlocked, color: '#2563eb' }, { label: 'Texts Blocked', value: context.data.spamStats.textsBlocked, color: '#2563eb' }, { label: 'Total Events', value: context.data.spamStats.totalSpamEvents, color: '#2563eb' }, { label: 'False Positives', value: context.data.spamStats.falsePositives, color: '#64748b' }, ], y); // Voice Protection y = this.drawSectionHeader(doc, 'Voice Protection', y); y = this.drawStatGrid(doc, [ { label: 'Analyses Run', value: context.data.voiceStats.analysesRun, color: '#2563eb' }, { label: 'Threats Detected', value: context.data.voiceStats.threatsDetected, color: '#ef4444' }, { label: 'Active Enrollments', value: context.data.voiceStats.enrollmentsActive, color: '#2563eb' }, { label: 'Synthetic Voices', value: context.data.voiceStats.syntheticDetections, color: '#eab308' }, ], y); // Home Title Monitoring (Premium only) if (context.data.homeTitleStats) { y = this.drawSectionHeader(doc, 'Home Title Monitoring', y); y = this.drawStatGrid(doc, [ { label: 'Properties Monitored', value: context.data.homeTitleStats.propertiesMonitored, color: '#2563eb' }, { label: 'Changes Detected', value: context.data.homeTitleStats.changesDetected, color: '#eab308' }, { label: 'Alerts Triggered', value: context.data.homeTitleStats.alertsTriggered, color: '#2563eb' }, ], y); } // Recommendations if (context.data.recommendations.length > 0) { y = this.drawSectionHeader(doc, 'Recommendations', y); for (const rec of context.data.recommendations) { const priorityColor = rec.priority === 'high' ? '#ef4444' : rec.priority === 'medium' ? '#eab308' : '#22c55e'; doc .rect(40, y, 4, 30) .fill(priorityColor) .fillColor('#1a202c') .font(StandardFonts.HelveticaBold) .fontSize(12) .text(rec.title, 50, y + 2, { width: w - 100 }) .font(StandardFonts.Helvetica) .fontSize(10) .fillColor('#475569') .text(rec.description, 50, y + 18, { width: w - 100 }); y += 45; } } // Footer doc .rect(0, h - 60, w, 60) .fill('#f5f7fa') .fillColor('#94a3b8') .fontSize(10) .font(StandardFonts.Helvetica) .text('ShieldAI — Your Digital Identity Protection', 40, h - 45, { align: 'center' }) .text(`Report ID: ${context.reportId}`, 40, h - 30, { align: 'center' }); doc.end(); }); } private drawSectionHeader(doc: PDFDocument, title: string, y: number): number { if (y > 680) { doc.addPage(); y = 40; } doc .fillColor('#1e40af') .fontSize(16) .font(StandardFonts.HelveticaBold) .text(title, 40, y) .rect(40, y + 18, 480, 2) .fill('#e2e8f0'); return y + 30; } private drawStatGrid( doc: PDFDocument, stats: Array<{ label: string; value: number; color: string }>, y: number ): number { const cols = Math.min(stats.length, 4); const colWidth = (doc.page.width - 80) / cols; for (let i = 0; i < stats.length; i += cols) { for (let j = 0; j < cols && i + j < stats.length; j++) { const stat = stats[i + j]; const x = 40 + j * colWidth; doc .rect(x, y, colWidth - 8, 60) .fill('#f8fafc') .fillColor(stat.color) .fontSize(20) .font(StandardFonts.HelveticaBold) .text(String(stat.value), x + 4, y + 8, { width: colWidth - 16, align: 'center' }) .fillColor('#64748b') .fontSize(9) .font(StandardFonts.Helvetica) .text(stat.label, x + 4, y + 35, { width: colWidth - 16, align: 'center' }); } y += 70; } return y + 10; } } export const pdfGenerator = new PdfGenerator();