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:
2026-04-29 18:16:47 -04:00
parent 29303799ed
commit ece783713e
6 changed files with 333 additions and 41 deletions

View 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);
}

View File

@@ -6,8 +6,13 @@ export {
ConfidenceLevel,
spamFeatureFlags,
spamRateLimits,
checkFlag,
isFeatureEnabled,
} from './spamshield.config';
// Feature flags
export * from './feature-flags';
// Services
export {
NumberReputationService,

View File

@@ -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

View File

@@ -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}`;
}
}

View File

@@ -8,8 +8,12 @@ export {
audioPreprocessingConfig,
voicePrintFeatureFlags,
voicePrintRateLimits,
checkFlag,
isFeatureEnabled,
} from './voiceprint.config';
// Services
export {
AudioPreprocessor,

View File

@@ -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