diff --git a/apps/api/src/services/spamshield/feature-flags.ts b/apps/api/src/services/spamshield/feature-flags.ts new file mode 100644 index 000000000..5c72e6aab --- /dev/null +++ b/apps/api/src/services/spamshield/feature-flags.ts @@ -0,0 +1,227 @@ +/** + * Feature Flag Management System + * Centralized feature flag handling with type safety and runtime updates + */ + +import type { z } from 'zod'; + +/** + * Type for feature flag values + */ +export type FeatureFlagValue = boolean | string | number; + +/** + * Interface for a feature flag definition + */ +export interface FeatureFlag { + key: string; + defaultValue: T; + description?: string; + allowedValues?: T[]; // For enum-like flags + category?: string; +} + +/** + * Feature flag registry - stores all defined flags + */ +export interface FeatureFlagRegistry { + [key: string]: FeatureFlag; +} + +/** + * Feature flag resolver - handles flag resolution logic + */ +export class FeatureFlagResolver { + private flags: FeatureFlagRegistry; + private resolvedCache: Map = new Map(); + + constructor(flags: FeatureFlagRegistry) { + this.flags = flags; + } + + /** + * Resolve a feature flag value + * Priority: Environment > Cache > Default + */ + resolve(key: string, defaultValue: T): T { + // Check cache first + if (this.resolvedCache.has(key)) { + return this.resolvedCache.get(key)! as T; + } + + // Check environment variable (allows runtime updates) + const envValue = process.env[`FLAG_${key.toUpperCase()}`]; + if (envValue !== undefined) { + // Try to parse as JSON first, then as boolean, then as string + let parsed: FeatureFlagValue; + try { + parsed = JSON.parse(envValue); + } catch { + parsed = envValue.toLowerCase() === 'true' ? true : + envValue.toLowerCase() === 'false' ? false : + envValue; + } + + // Validate against allowed values if defined + const flag = this.flags[key]; + if (flag && flag.allowedValues && !flag.allowedValues.includes(parsed)) { + console.warn(`Invalid value for flag ${key}: ${parsed}. Using default.`); + parsed = defaultValue as FeatureFlagValue; + } + + this.resolvedCache.set(key, parsed); + return parsed as T; + } + + // Use cached value if available + if (this.resolvedCache.has(key)) { + return this.resolvedCache.get(key)! as T; + } + + // Return default + this.resolvedCache.set(key, defaultValue as FeatureFlagValue); + return defaultValue as T; + } + + /** + * Check if a flag is enabled (boolean check) + */ + isEnabled(key: string, defaultValue: T): T { + return this.resolve(key, defaultValue) as T; + } + + /** + * Get flag definition + */ + getDefinition(key: string): FeatureFlag | undefined { + return this.flags[key]; + } + + /** + * List all registered flags + */ + getAllFlags(): FeatureFlagRegistry { + return { ...this.flags }; + } + + /** + * Clear the resolution cache (useful for testing) + */ + clearCache(): void { + this.resolvedCache.clear(); + } +} + +/** + * Feature flag configuration with pre-defined flags + */ +export const featureFlags: FeatureFlagRegistry = { + // SpamShield Feature Flags + 'spamshield.enable.number.reputation': { + key: 'spamshield_enable_number_reputation', + defaultValue: true, + description: 'Enable number reputation checking (Hiya API integration)', + category: 'spamshield', + }, + 'spamshield.enable.content.classification': { + key: 'spamshield_enable_content_classification', + defaultValue: true, + description: 'Enable SMS content classification (BERT model)', + category: 'spamshield', + }, + 'spamshield.enable.behavioral.analysis': { + key: 'spamshield_enable_behavioral_analysis', + defaultValue: true, + description: 'Enable call behavioral analysis', + category: 'spamshield', + }, + 'spamshield.enable.community.intelligence': { + key: 'spamshield_enable_community_intelligence', + defaultValue: true, + description: 'Enable community intelligence sharing', + category: 'spamshield', + }, + 'spamshield.enable.real.time.blocking': { + key: 'spamshield_enable_real_time_blocking', + defaultValue: true, + description: 'Enable real-time spam blocking', + category: 'spamshield', + }, + 'spamshield.enable.multiple.sources': { + key: 'spamshield_enable_multiple_sources', + defaultValue: false, + description: 'Enable multiple reputation source aggregation (Truecaller, etc.)', + category: 'spamshield', + }, + 'spamshield.enable.ml.classifier': { + key: 'spamshield_enable_ml_classifier', + defaultValue: false, + description: 'Enable ML-based spam classification', + category: 'spamshield', + }, + + // VoicePrint Feature Flags + 'voiceprint.enable.ml.service': { + key: 'voiceprint_enable_ml_service', + defaultValue: false, + description: 'Enable ML service integration for voice analysis', + category: 'voiceprint', + }, + 'voiceprint.enable.faiss.index': { + key: 'voiceprint_enable_faiss_index', + defaultValue: true, + description: 'Enable FAISS index for voice matching', + category: 'voiceprint', + }, + 'voiceprint.enable.batch.analysis': { + key: 'voiceprint_enable_batch_analysis', + defaultValue: true, + description: 'Enable batch voice analysis', + category: 'voiceprint', + }, + 'voiceprint.enable.realtime.analysis': { + key: 'voiceprint_enable_realtime_analysis', + defaultValue: false, + description: 'Enable real-time voice analysis', + category: 'voiceprint', + }, + 'voiceprint.enable.mock.model': { + key: 'voiceprint_enable_mock_model', + defaultValue: true, + description: 'Enable mock model for development', + category: 'voiceprint', + }, + + // General Platform Flags + 'platform.enable.audit.logs': { + key: 'platform_enable_audit_logs', + defaultValue: true, + description: 'Enable comprehensive audit logging', + category: 'platform', + }, + 'platform.enable.kpi.tracking': { + key: 'platform_enable_kpi_tracking', + defaultValue: true, + description: 'Enable KPI snapshot tracking', + category: 'platform', + }, +}; + +/** + * Create a resolver instance with the default flags + */ +export const featureFlagResolver = new FeatureFlagResolver(featureFlags); + +/** + * Convenience function for quick flag checks + */ +export function isFeatureEnabled(key: string, defaultValue: T): T { + return featureFlagResolver.isEnabled(key, defaultValue); +} + +/** + * Check if a flag is enabled with type safety + */ +export function checkFlag(key: string, defaultValue: T): T { + return featureFlagResolver.resolve(key, defaultValue); +} diff --git a/apps/api/src/services/spamshield/index.ts b/apps/api/src/services/spamshield/index.ts index ad731c977..917e23787 100644 --- a/apps/api/src/services/spamshield/index.ts +++ b/apps/api/src/services/spamshield/index.ts @@ -6,8 +6,13 @@ export { ConfidenceLevel, spamFeatureFlags, spamRateLimits, + checkFlag, + isFeatureEnabled, } from './spamshield.config'; +// Feature flags +export * from './feature-flags'; + // Services export { NumberReputationService, diff --git a/apps/api/src/services/spamshield/spamshield.config.ts b/apps/api/src/services/spamshield/spamshield.config.ts index 5ad428c40..ad2f1c09b 100644 --- a/apps/api/src/services/spamshield/spamshield.config.ts +++ b/apps/api/src/services/spamshield/spamshield.config.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { checkFlag } from './feature-flags'; // Environment variables for SpamShield const envSchema = z.object({ @@ -46,12 +47,16 @@ export enum ConfidenceLevel { } // Feature flags for spam detection +// Use the centralized feature flag system from feature-flags.ts +// These are aliases for quick access export const spamFeatureFlags = { - enableNumberReputation: true, - enableContentClassification: true, - enableBehavioralAnalysis: true, - enableCommunityIntelligence: true, - enableRealTimeBlocking: true, + enableNumberReputation: checkFlag('spamshield.enable.number.reputation', true), + enableContentClassification: checkFlag('spamshield.enable.content.classification', true), + enableBehavioralAnalysis: checkFlag('spamshield.enable.behavioral.analysis', true), + enableCommunityIntelligence: checkFlag('spamshield.enable.community.intelligence', true), + enableRealTimeBlocking: checkFlag('spamshield.enable.real.time.blocking', true), + enableMultipleSources: checkFlag('spamshield.enable.multiple.sources', false), + enableMLClassifier: checkFlag('spamshield.enable.ml.classifier', false), }; // Rate limits for spam analysis diff --git a/apps/api/src/services/spamshield/spamshield.service.ts b/apps/api/src/services/spamshield/spamshield.service.ts index e1a2fcd67..f31000ba4 100644 --- a/apps/api/src/services/spamshield/spamshield.service.ts +++ b/apps/api/src/services/spamshield/spamshield.service.ts @@ -1,5 +1,7 @@ import { prisma, SpamRule, SpamFeedback, User } from '@shieldsai/shared-db'; -import { spamShieldEnv, SpamDecision, ConfidenceLevel } from './spamshield.config'; +import { spamShieldEnv, SpamDecision, ConfidenceLevel, spamFeatureFlags } from './spamshield.config'; +import { checkFlag } from './feature-flags'; +import { createHash } from 'crypto'; // Number reputation service (Hiya API integration) export class NumberReputationService { @@ -13,6 +15,15 @@ export class NumberReputationService { 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 }, @@ -45,6 +56,15 @@ export class NumberReputationService { truecaller: { isSpam: boolean; confidence: number } | null; combinedScore: number; }> { + // Only enable if feature flag is set + if (!spamFeatureFlags.enableMultipleSources) { + return { + hiya: { isSpam: false, confidence: 0.0 }, + truecaller: null, + combinedScore: 0.0, + }; + } + const hiyaResult = await this.checkReputation(phoneNumber); let truecallerResult: { isSpam: boolean; confidence: number } | null = null; @@ -58,7 +78,7 @@ export class NumberReputationService { // Weighted average: Hiya 70%, Truecaller 30% const combinedScore = hiyaResult.confidence * 0.7 + - (truecallerResult?.confidence ?? 0) * 0.3; + (truecallerResult?.confidence ?? 0) * 0.3; return { hiya: { isSpam: hiyaResult.isSpam, confidence: hiyaResult.confidence }, @@ -89,6 +109,20 @@ export class SMSClassifierService { 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; + + return { + isSpam, + confidence, + spamFeatures: features, + }; + } + if (!this.model) { await this.initialize(); } @@ -171,31 +205,35 @@ export class CallAnalysisService { const reasons: string[] = []; let spamScore = 0.0; - // Number reputation check - const reputationService = new NumberReputationService(); - const reputation = await reputationService.checkMultiSource(callData.phoneNumber); - - if (reputation.combinedScore > 0.7) { - spamScore += reputation.combinedScore * 0.4; - reasons.push('high_spam_reputation'); + // 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 > 0.7) { + spamScore += reputation.combinedScore * 0.4; + reasons.push('high_spam_reputation'); + } } - // Behavioral analysis - if (callData.duration && callData.duration < 10) { - spamScore += 0.2; - reasons.push('short_duration'); - } + // Behavioral analysis - only if feature flag enabled + if (spamFeatureFlags.enableBehavioralAnalysis) { + if (callData.duration && callData.duration < 10) { + spamScore += 0.2; + reasons.push('short_duration'); + } - if (callData.isVoip) { - spamScore += 0.15; - reasons.push('voip_number'); - } + if (callData.isVoip) { + spamScore += 0.15; + reasons.push('voip_number'); + } - // Time-of-day anomaly (simplified) - const hour = callData.callTime.getHours(); - if (hour < 6 || hour > 22) { - spamScore += 0.1; - reasons.push('unusual_hours'); + // Time-of-day anomaly (simplified) + const hour = callData.callTime.getHours(); + if (hour < 6 || hour > 22) { + spamScore += 0.1; + reasons.push('unusual_hours'); + } } // Determine decision @@ -228,6 +266,23 @@ export class SpamFeedbackService { confidence?: number, metadata?: Record ): Promise { + // 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, + feedbackType: 'user_confirmation' as const, + metadata, + createdAt: new Date(), + updatedAt: new Date(), + }; + } + const phoneNumberHash = this.hashPhoneNumber(phoneNumber); const feedback = await prisma.spamFeedback.create({ @@ -290,13 +345,9 @@ export class SpamFeedbackService { } private hashPhoneNumber(phoneNumber: string): string { - // Simple hash for demonstration - let hash = 0; - for (let i = 0; i < phoneNumber.length; i++) { - hash = ((hash << 5) - hash) + phoneNumber.charCodeAt(i); - hash |= 0; - } - return `hash_${Math.abs(hash)}`; + // SHA-256 hash for phone number fingerprinting + const hash = createHash('sha256').update(phoneNumber).digest('hex'); + return `sha256_${hash}`; } } diff --git a/apps/api/src/services/voiceprint/index.ts b/apps/api/src/services/voiceprint/index.ts index a61e057d0..4d40bf7fb 100644 --- a/apps/api/src/services/voiceprint/index.ts +++ b/apps/api/src/services/voiceprint/index.ts @@ -8,8 +8,12 @@ export { audioPreprocessingConfig, voicePrintFeatureFlags, voicePrintRateLimits, + checkFlag, + isFeatureEnabled, } from './voiceprint.config'; + + // Services export { AudioPreprocessor, diff --git a/apps/api/src/services/voiceprint/voiceprint.config.ts b/apps/api/src/services/voiceprint/voiceprint.config.ts index 2b86b817f..0904dd3d1 100644 --- a/apps/api/src/services/voiceprint/voiceprint.config.ts +++ b/apps/api/src/services/voiceprint/voiceprint.config.ts @@ -72,13 +72,13 @@ export const audioPreprocessingConfig = { maxSilenceDurationMs: 500, }; -// Feature flags +// Feature flags - use centralized system export const voicePrintFeatureFlags = { - enableMLService: false, - enableFAISSIndex: true, - enableBatchAnalysis: true, - enableRealtimeAnalysis: false, - enableMockModel: true, + enableMLService: checkFlag('voiceprint.enable.ml.service', false), + enableFAISSIndex: checkFlag('voiceprint.enable.faiss.index', true), + enableBatchAnalysis: checkFlag('voiceprint.enable.batch.analysis', true), + enableRealtimeAnalysis: checkFlag('voiceprint.enable.realtime.analysis', false), + enableMockModel: checkFlag('voiceprint.enable.mock.model', true), }; // Rate limits for voice analysis