FRE-4510: Implement feature flag checks for spam classification
- Create centralized feature flag management system (feature-flags.ts) - Add 15 feature flags across SpamShield, VoicePrint, and Platform categories - Update spamshield.config.ts to use checkFlag() for all flags - Add feature flag checks to all spamshield.service.ts methods: * NumberReputationService.checkReputation() * NumberReputationService.checkMultiSource() * SMSClassifierService.classify() * CallAnalysisService.analyzeCall() * SpamFeedbackService.recordFeedback() - Update index.ts exports to include feature flag utilities - Flags support runtime updates via FLAG_<KEY> environment variables Flags implemented: - SpamShield: enableNumberReputation, enableContentClassification, enableBehavioralAnalysis, enableCommunityIntelligence, enableRealTimeBlocking, enableMultipleSources, enableMLClassifier - VoicePrint: enableMLService, enableFAISSIndex, enableBatchAnalysis, enableRealtimeAnalysis, enableMockModel - Platform: enableAuditLogs, enableKPITracking
This commit is contained in:
227
apps/api/src/services/spamshield/feature-flags.ts
Normal file
227
apps/api/src/services/spamshield/feature-flags.ts
Normal file
@@ -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<T = FeatureFlagValue> {
|
||||
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<string, FeatureFlagValue> = new Map();
|
||||
|
||||
constructor(flags: FeatureFlagRegistry) {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a feature flag value
|
||||
* Priority: Environment > Cache > Default
|
||||
*/
|
||||
resolve<T>(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<T>(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<T>(key: string, defaultValue: T): T {
|
||||
return featureFlagResolver.isEnabled(key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a flag is enabled with type safety
|
||||
*/
|
||||
export function checkFlag<T>(key: string, defaultValue: T): T {
|
||||
return featureFlagResolver.resolve(key, defaultValue);
|
||||
}
|
||||
@@ -6,8 +6,13 @@ export {
|
||||
ConfidenceLevel,
|
||||
spamFeatureFlags,
|
||||
spamRateLimits,
|
||||
checkFlag,
|
||||
isFeatureEnabled,
|
||||
} from './spamshield.config';
|
||||
|
||||
// Feature flags
|
||||
export * from './feature-flags';
|
||||
|
||||
// Services
|
||||
export {
|
||||
NumberReputationService,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, any>
|
||||
): Promise<SpamFeedback> {
|
||||
// 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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@ export {
|
||||
audioPreprocessingConfig,
|
||||
voicePrintFeatureFlags,
|
||||
voicePrintRateLimits,
|
||||
checkFlag,
|
||||
isFeatureEnabled,
|
||||
} from './voiceprint.config';
|
||||
|
||||
|
||||
|
||||
// Services
|
||||
export {
|
||||
AudioPreprocessor,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user