import { prisma, SpamFeedback } from '@shieldai/db'; import { spamShieldEnv, SpamDecision, spamFeatureFlags, defaultScores, metadataLimits } from './spamshield.config'; import { createHash } from 'crypto'; import { spamAuditLogger, hashPhoneNumber } from './spamshield.audit-logger'; // Number reputation service (Hiya API integration) export class NumberReputationService { /** * Check number reputation using Hiya API */ async checkReputation(phoneNumber: string): Promise<{ isSpam: boolean; confidence: number; spamType?: string; reportCount: number; }> { try { // Only enable if feature flag is set if (!spamFeatureFlags.enableNumberReputation) { return { isSpam: false, confidence: 0.0, reportCount: 0, }; } // TODO: Integrate with Hiya API // const response = await fetch(`${spamShieldEnv.HIYA_API_URL}/lookup`, { // headers: { 'X-API-Key': spamShieldEnv.HIYA_API_KEY }, // method: 'POST', // body: JSON.stringify({ phone: phoneNumber }), // }); // Simulated response for now return { isSpam: false, confidence: defaultScores.defaultReputationLowConfidence, spamType: undefined, reportCount: 0, }; } catch (error) { console.error('Error checking number reputation:', error); return { isSpam: false, confidence: defaultScores.defaultReputationConfidence, reportCount: 0, }; } } /** * Check number against multiple reputation sources */ async checkMultiSource(phoneNumber: string): Promise<{ hiya: { isSpam: boolean; confidence: number }; truecaller: { isSpam: boolean; confidence: number } | null; combinedScore: number; }> { // Only enable if feature flag is set if (!spamFeatureFlags.enableMultipleSources) { return { hiya: { isSpam: false, confidence: defaultScores.defaultReputationConfidence }, truecaller: null, combinedScore: defaultScores.defaultSpamScore, }; } const hiyaResult = await this.checkReputation(phoneNumber); let truecallerResult: { isSpam: boolean; confidence: number } | null = null; if (spamShieldEnv.TRUECALLER_API_KEY) { // TODO: Integrate Truecaller truecallerResult = { isSpam: false, confidence: defaultScores.defaultReputationConfidence, }; } // Weighted average: Hiya 70%, Truecaller 30% const combinedScore = hiyaResult.confidence * defaultScores.hiyaWeightInCombinedScore + (truecallerResult?.confidence ?? defaultScores.defaultReputationConfidence) * defaultScores.truecallerWeightInCombinedScore; return { hiya: { isSpam: hiyaResult.isSpam, confidence: hiyaResult.confidence }, truecaller: truecallerResult, combinedScore, }; } } // SMS content classifier (BERT-based) export class SMSClassifierService { private model: any = null; // BERT model placeholder private _initPromise: Promise | null = null; /** * Initialize the BERT model (thread-safe via promise deduplication) */ async initialize(): Promise { // TODO: Load BERT model from path // this.model = await loadBERTModel(spamShieldEnv.BERT_MODEL_PATH); console.log('SMS classifier initialized'); } /** * Ensures model is initialized before use. Concurrent callers * await the same initialization promise to avoid race conditions. */ private async ensureInitialized(): Promise { if (this._initPromise) { return this._initPromise; } this._initPromise = (async () => { if (this.model) { return; } await this.initialize(); })(); return this._initPromise; } /** * Classify SMS text as spam or ham */ async classify( smsText: string, phoneNumber?: string ): Promise<{ isSpam: boolean; confidence: number; spamFeatures: string[]; }> { // Only enable if feature flag is set if (!spamFeatureFlags.enableMLClassifier) { // Return basic feature-based classification const features = this.extractFeatures(smsText); const confidence = this.calculateConfidence(features); const isSpam = confidence >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK; spamAuditLogger.logClassification({ type: 'sms', phoneNumberHash: phoneNumber ? hashPhoneNumber(phoneNumber) : 'unknown', decision: isSpam ? 'spam' : 'ham', confidence, reasons: features, featureFlags: { enableMLClassifier: spamFeatureFlags.enableMLClassifier }, }); return { isSpam, confidence, spamFeatures: features, }; } await this.ensureInitialized(); // Extract features const features = this.extractFeatures(smsText); // TODO: Run through BERT model // const prediction = await this.model.predict(smsText); // Simulated prediction const confidence = this.calculateConfidence(features); const isSpam = confidence >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK; spamAuditLogger.logClassification({ type: 'sms', phoneNumberHash: phoneNumber ? hashPhoneNumber(phoneNumber) : 'unknown', decision: isSpam ? 'spam' : 'ham', confidence, reasons: features, featureFlags: { enableMLClassifier: spamFeatureFlags.enableMLClassifier }, }); return { isSpam, confidence, spamFeatures: features, }; } private extractFeatures(text: string): string[] { const features: string[] = []; const lowerText = text.toLowerCase(); // URL presence if (/(http|www)\./i.test(text)) { features.push('url_present'); } // Emoji density const emojiCount = (text.match(/[\p{Emoji}]/gu) || []).length; if (emojiCount / text.length > 0.1) { features.push('high_emoji_density'); } // Urgency keywords const urgencyWords = ['now', 'urgent', 'limited', 'act fast', 'today']; if (urgencyWords.some(word => lowerText.includes(word))) { features.push('urgency_keyword'); } // Excessive capitalization if (/[A-Z]{3,}/.test(text)) { features.push('excessive_caps'); } return features; } private calculateConfidence(features: string[]): number { const baseConfidence = defaultScores.defaultBaseConfidence; const featureWeights: Record = { url_present: defaultScores.featureWeights.urlPresent, high_emoji_density: defaultScores.featureWeights.highEmojiDensity, urgency_keyword: defaultScores.featureWeights.urgencyKeyword, excessive_caps: defaultScores.featureWeights.excessiveCaps, }; return Math.min(defaultScores.defaultMaxConfidence, baseConfidence + features.reduce((sum, f) => sum + (featureWeights[f] || 0), 0)); } } // Call analysis service export class CallAnalysisService { /** * Analyze incoming call for spam indicators */ async analyzeCall(callData: { phoneNumber: string; duration?: number; callTime: Date; isVoip?: boolean; }): Promise<{ decision: SpamDecision; confidence: number; reasons: string[]; }> { const reasons: string[] = []; let spamScore = defaultScores.defaultSpamScore; // Number reputation check - only if feature flag enabled if (spamFeatureFlags.enableBehavioralAnalysis) { const reputationService = new NumberReputationService(); const reputation = await reputationService.checkMultiSource(callData.phoneNumber); if (reputation.combinedScore > defaultScores.highReputationThreshold) { spamScore += reputation.combinedScore * defaultScores.reputationWeightInCombinedScore; reasons.push('high_spam_reputation'); } } // Behavioral analysis - only if feature flag enabled if (spamFeatureFlags.enableBehavioralAnalysis) { if (callData.duration && callData.duration < 10) { spamScore += defaultScores.shortDurationScore; reasons.push('short_duration'); } if (callData.isVoip) { spamScore += defaultScores.voipScore; reasons.push('voip_number'); } // Time-of-day anomaly (simplified) const hour = callData.callTime.getHours(); if (hour < 6 || hour > 22) { spamScore += defaultScores.unusualHoursScore; reasons.push('unusual_hours'); } } // Determine decision let decision: SpamDecision; if (spamScore >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK) { decision = SpamDecision.BLOCK; } else if (spamScore >= spamShieldEnv.SPAM_THRESHOLD_FLAG) { decision = SpamDecision.FLAG; } else { decision = SpamDecision.ALLOW; } spamAuditLogger.logClassification({ type: 'call', phoneNumberHash: hashPhoneNumber(callData.phoneNumber), decision: decision.toLowerCase() as 'block' | 'flag' | 'allow', confidence: spamScore, reasons, featureFlags: { enableBehavioralAnalysis: spamFeatureFlags.enableBehavioralAnalysis, enableNumberReputation: spamFeatureFlags.enableNumberReputation, }, metadata: { duration: callData.duration, isVoip: callData.isVoip, callTime: callData.callTime.toISOString(), }, }); return { decision, confidence: spamScore, reasons, }; } } // User feedback service export class SpamFeedbackService { /** * Validate metadata size against defined limits */ private validateMetadata(metadata?: Record): { isValid: boolean; trimmedMetadata?: Record; reasons?: string[]; } { if (!metadata) { return { isValid: true }; } const reasons: string[] = []; let trimmedMetadata: Record = metadata; // Check number of keys const keyCount = Object.keys(metadata).length; if (keyCount > metadataLimits.maxMetadataKeys) { reasons.push(`Metadata has ${keyCount} keys, exceeding limit of ${metadataLimits.maxMetadataKeys}`); trimmedMetadata = Object.entries(metadata).slice(0, metadataLimits.maxMetadataKeys); } // Check total JSON size const jsonSize = JSON.stringify(metadata).length; if (jsonSize > metadataLimits.maxMetadataSizeBytes) { reasons.push(`Metadata size ${jsonSize} bytes exceeds limit of ${metadataLimits.maxMetadataSizeBytes} bytes`); // Truncate long values trimmedMetadata = Object.fromEntries( Object.entries(metadata).map(([key, value]) => { const valueStr = String(value); if (valueStr.length > metadataLimits.maxMetadataValueSizeBytes) { return [key, valueStr.slice(0, metadataLimits.maxMetadataValueSizeBytes)]; } return [key, value]; }) ); } return { isValid: reasons.length === 0, trimmedMetadata, reasons: reasons.length > 0 ? reasons : undefined, }; } /** * Record user feedback on spam detection */ async recordFeedback( userId: string, phoneNumber: string, isSpam: boolean, confidence?: number, metadata?: Record ): Promise { // Defensive null checks for required fields if (!userId || typeof userId !== 'string' || userId.trim().length === 0) { throw new Error('Feedback: userId is required'); } if (!phoneNumber || typeof phoneNumber !== 'string' || phoneNumber.trim().length === 0) { throw new Error('Feedback: phoneNumber is required'); } if (typeof isSpam !== 'boolean') { throw new Error('Feedback: isSpam must be a boolean'); } // Validate confidence range if provided const validatedConfidence = confidence !== undefined && confidence !== null ? (Number.isFinite(confidence) && confidence >= 0 && confidence <= 1 ? confidence : undefined) : undefined; // Treat null metadata as undefined const effectiveMetadata = metadata !== null ? metadata : undefined; const validation = this.validateMetadata(effectiveMetadata); const validatedMetadata = validation.trimmedMetadata; // Only enable if feature flag is set if (!spamFeatureFlags.enableCommunityIntelligence) { // Return a mock feedback for development return { id: `mock_${Date.now()}`, userId, phoneNumber, phoneNumberHash: this.hashPhoneNumber(phoneNumber), isSpam, confidence: validatedConfidence, feedbackType: 'user_confirmation' as const, metadata: validatedMetadata, createdAt: new Date(), updatedAt: new Date(), }; } const phoneNumberHash = this.hashPhoneNumber(phoneNumber); const feedback = await prisma.spamFeedback.create({ data: { userId, phoneNumber, phoneNumberHash, isSpam, confidence: validatedConfidence, feedbackType: 'user_confirmation', metadata: validatedMetadata, }, }); return feedback; } /** * Get spam history for a user */ async getSpamHistory( userId: string, options?: { limit?: number; isSpam?: boolean; startDate?: Date; } ): Promise { return prisma.spamFeedback.findMany({ where: { userId, ...(options?.isSpam !== undefined && { isSpam: options.isSpam }), ...(options?.startDate && { createdAt: { gte: options.startDate } }), }, orderBy: { createdAt: 'desc' }, take: options?.limit ?? 100, }); } /** * Get statistics for a user */ async getStatistics(userId: string): Promise<{ totalAnalyses: number; spamCount: number; hamCount: number; spamPercentage: number; }> { const [total, spam] = await Promise.all([ prisma.spamFeedback.count({ where: { userId } }), prisma.spamFeedback.count({ where: { userId, isSpam: true } }), ]); return { totalAnalyses: total, spamCount: spam, hamCount: total - spam, spamPercentage: total > 0 ? (spam / total) * 100 : 0, }; } private hashPhoneNumber(phoneNumber: string): string { // SHA-256 hash for phone number fingerprinting const hash = createHash('sha256').update(phoneNumber).digest('hex'); return `sha256_${hash}`; } } // Export instances export const numberReputationService = new NumberReputationService(); export const smsClassifierService = new SMSClassifierService(); export const callAnalysisService = new CallAnalysisService(); export const spamFeedbackService = new SpamFeedbackService();