FRE-4529: Transfer ShieldAI code from FrenoCorp repo

Transferred ShieldAI-related files mistakenly placed in ~/code/FrenoCorp:
- Services: spamshield (feature-flags, audit-logger, error-handler), voiceprint (config, service, feature-flags), darkwatch (pipeline, scan, scheduler, watchlist, webhook)
- Packages: shared-analytics, shared-auth, shared-ui, shared-utils (new); shared-billing, jobs supplemented with unique FC files
- Server: alerts (FC version newer), routes (spamshield, darkwatch, voiceprint)
- Config: turbo.json, tsconfig.base.json, vite/vitest configs, drizzle, Dockerfile
- VoicePrint ML service
- Examples

Pending: apps/{api,web,mobile}/ structured merge, shared-db/db mapping

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-02 10:13:13 -04:00
parent 8687868632
commit 1e42c4a5c2
45 changed files with 4837 additions and 562 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

@@ -0,0 +1,118 @@
import { createHash } from 'crypto';
export type AuditClassificationType = 'sms' | 'call';
export interface AuditClassificationEntry {
id: string;
timestamp: string;
type: AuditClassificationType;
phoneNumberHash: string;
decision: 'spam' | 'ham' | 'block' | 'flag' | 'allow';
confidence: number;
reasons: string[];
featureFlags: Record<string, boolean>;
metadata?: Record<string, unknown>;
}
const MAX_AUDIT_LOG_SIZE = 10_000;
class AuditLogger {
private entries: AuditClassificationEntry[] = [];
logClassification(entry: Omit<AuditClassificationEntry, 'id' | 'timestamp'>): AuditClassificationEntry {
const record: AuditClassificationEntry = {
id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
timestamp: new Date().toISOString(),
...entry,
};
this.entries.push(record);
if (this.entries.length > MAX_AUDIT_LOG_SIZE) {
this.entries.shift();
}
console.log(
`[SpamShield:Audit] type=${record.type} decision=${record.decision} ` +
`confidence=${record.confidence.toFixed(3)} reasons=${record.reasons.join(',') || 'none'} ` +
`phoneHash=${record.phoneNumberHash}`
);
return record;
}
getEntries(
filters?: {
type?: AuditClassificationType;
decision?: string;
startDate?: Date;
endDate?: Date;
limit?: number;
}
): AuditClassificationEntry[] {
let results = this.entries;
if (filters?.type) {
results = results.filter(e => e.type === filters.type);
}
if (filters?.decision) {
results = results.filter(e => e.decision === filters.decision);
}
if (filters?.startDate) {
results = results.filter(e => new Date(e.timestamp) >= filters.startDate!);
}
if (filters?.endDate) {
results = results.filter(e => new Date(e.timestamp) <= filters.endDate!);
}
if (filters?.limit) {
results = results.slice(-filters.limit);
}
return results;
}
getSummary(): {
totalEntries: number;
spamCount: number;
hamCount: number;
blockCount: number;
flagCount: number;
allowCount: number;
avgConfidence: number;
} {
const spamCount = this.entries.filter(e => e.decision === 'spam' || e.decision === 'block').length;
const hamCount = this.entries.filter(e => e.decision === 'ham' || e.decision === 'allow').length;
const blockCount = this.entries.filter(e => e.decision === 'block').length;
const flagCount = this.entries.filter(e => e.decision === 'flag').length;
const allowCount = this.entries.filter(e => e.decision === 'allow').length;
const avgConfidence =
this.entries.length > 0
? this.entries.reduce((s, e) => s + e.confidence, 0) / this.entries.length
: 0;
return {
totalEntries: this.entries.length,
spamCount,
hamCount,
blockCount,
flagCount,
allowCount,
avgConfidence: Math.round(avgConfidence * 1000) / 1000,
};
}
clear(): void {
this.entries = [];
}
}
export const spamAuditLogger = new AuditLogger();
export function hashPhoneNumber(phoneNumber: string): string {
const hash = createHash('sha256').update(phoneNumber.trim()).digest('hex');
return `sha256_${hash}`;
}

View File

@@ -0,0 +1,118 @@
import { FastifyReply } from 'fastify';
import { SpamErrorCode, HttpStatus, SpamErrorResponse } from './spamshield.config';
export { SpamErrorCode, HttpStatus };
export type { SpamErrorResponse };
/**
* Standardized error response builder for SpamShield API
*/
export class ErrorHandler {
/**
* Create a standard error response
*/
static create(
code: SpamErrorCode,
message: string,
options?: {
field?: string;
requestId?: string;
additionalData?: Record<string, unknown>;
}
): SpamErrorResponse {
return {
error: {
code,
message,
...(options?.field && { field: options.field }),
timestamp: new Date().toISOString(),
...(options?.requestId && { requestId: options.requestId }),
},
};
}
/**
* Send a standard error response with appropriate HTTP status code
*/
static send(
reply: FastifyReply,
code: SpamErrorCode,
message: string,
options?: {
field?: string;
status?: number;
requestId?: string;
}
): void {
const status = options?.status ?? this.getStatusForCode(code);
const errorResponse = this.create(code, message, {
field: options?.field,
requestId: options?.requestId,
});
reply.code(status).send(errorResponse);
}
/**
* Map error codes to HTTP status codes
*/
private static getStatusForCode(code: SpamErrorCode): number {
const statusMap: Record<SpamErrorCode, number> = {
// Client errors
[SpamErrorCode.INVALID_REQUEST]: HttpStatus.BAD_REQUEST,
[SpamErrorCode.MISSING_REQUIRED_FIELD]: HttpStatus.BAD_REQUEST,
[SpamErrorCode.UNAUTHORIZED]: HttpStatus.UNAUTHORIZED,
[SpamErrorCode.NOT_FOUND]: HttpStatus.NOT_FOUND,
[SpamErrorCode.VALIDATION_ERROR]: HttpStatus.BAD_REQUEST,
// Server errors
[SpamErrorCode.CLASSIFICATION_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY,
[SpamErrorCode.REPUTATION_CHECK_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY,
[SpamErrorCode.ANALYSIS_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY,
[SpamErrorCode.FEEDBACK_RECORD_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY,
[SpamErrorCode.DATABASE_ERROR]: HttpStatus.INTERNAL_SERVER_ERROR,
[SpamErrorCode.RATE_LIMIT_EXCEEDED]: HttpStatus.TOO_MANY_REQUESTS,
[SpamErrorCode.SERVICE_UNAVAILABLE]: HttpStatus.SERVICE_UNAVAILABLE,
};
return statusMap[code] ?? HttpStatus.INTERNAL_SERVER_ERROR;
}
/**
* Validate required string field
*/
static validateRequiredField(
value: unknown,
fieldName: string
): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } {
if (!value || typeof value !== 'string' || value.trim() === '') {
return {
isValid: false,
error: {
code: SpamErrorCode.MISSING_REQUIRED_FIELD,
message: `${fieldName} is required`,
field: fieldName,
},
};
}
return { isValid: true };
}
/**
* Validate boolean field
*/
static validateBooleanField(
value: unknown,
fieldName: string
): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } {
if (value === undefined || value === null || typeof value !== 'boolean') {
return {
isValid: false,
error: {
code: SpamErrorCode.VALIDATION_ERROR,
message: `${fieldName} must be a boolean`,
field: fieldName,
},
};
}
return { isValid: true };
}
}