Files
ShieldAI/services/spamshield/src/carriers/carrier-factory.ts
Michael Freno 274afa6335 FRE-4499: Fix security review findings (S01-S06)
- S01 (High): Pre-compile regex patterns in RuleEngine.loadActiveRules() and
  cache them; eliminate per-evaluation RegExp construction in rule-engine.ts
  and spamshield.service.ts (ReDoS mitigation)
- S02 (High): SMS classifier now accepts optional senderPhoneNumber via
  SmsClassificationContext; reputation check uses actual sender instead of
  hardcoded 'placeholder'
- S03 (Medium): AlertServer (services/spamshield) now enforces JWT auth,
  origin allowlist, and max client limit on WebSocket connections
- S04 (Medium): hashPhoneNumber() uses SHA-256 (crypto.createHash) instead
  of reversible hex encoding (Buffer.toString('hex'))
- S05 (Medium): DecisionEngine.evaluate() wraps evaluation in Promise.race
  with configurable evaluationTimeout; returns fallback decision on timeout
- S06 (Medium): CarrierFactory.getAllCarriers() is now async and properly
  awaits isHealthy() promises instead of returning raw Promise objects

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-02 15:58:49 -04:00

111 lines
2.8 KiB
TypeScript

import { CarrierApi } from './carrier-types';
import { TwilioCarrier } from './twilio-carrier';
import { PlivoCarrier } from './plivo-carrier';
export type CarrierType = 'twilio' | 'plivo' | 'sip';
export interface CarrierFactoryConfig {
twilio?: {
apiKey: string;
apiSecret: string;
accountSid: string;
apiBaseUrl?: string;
decisionTimeout?: number;
};
plivo?: {
authId: string;
authToken: string;
apiBaseUrl?: string;
decisionTimeout?: number;
};
defaultDecisionTimeout?: number;
}
export class CarrierFactory {
private readonly config: CarrierFactoryConfig;
private readonly carriers: Map<CarrierType, CarrierApi> = new Map();
constructor(config: CarrierFactoryConfig) {
this.config = {
defaultDecisionTimeout: 200,
...config,
};
}
createCarrier(type: CarrierType): CarrierApi {
const cached = this.carriers.get(type);
if (cached) {
return cached;
}
const carrier = this.instantiateCarrier(type);
this.carriers.set(type, carrier);
return carrier;
}
async validateCarrier(type: CarrierType): Promise<boolean> {
const carrier = this.createCarrier(type);
return carrier.isHealthy();
}
async getCarrierMetrics(type: CarrierType): Promise<{
type: CarrierType;
healthy: boolean;
latency: number;
}> {
const carrier = this.createCarrier(type);
const startTime = Date.now();
const healthy = await carrier.isHealthy();
const latency = Date.now() - startTime;
return { type, healthy, latency };
}
private instantiateCarrier(type: CarrierType): CarrierApi {
switch (type) {
case 'twilio':
if (!this.config.twilio) {
throw new Error('Twilio configuration not provided');
}
return new TwilioCarrier({
...this.config.twilio,
decisionTimeout: this.config.twilio.decisionTimeout ?? this.config.defaultDecisionTimeout,
});
case 'plivo':
if (!this.config.plivo) {
throw new Error('Plivo configuration not provided');
}
return new PlivoCarrier({
...this.config.plivo,
decisionTimeout: this.config.plivo.decisionTimeout ?? this.config.defaultDecisionTimeout,
});
case 'sip':
// SIP carrier would be implemented separately
throw new Error('SIP carrier not yet implemented');
default:
throw new Error(`Unknown carrier type: ${type}`);
}
}
async getAllCarriers(): Promise<Array<{ type: CarrierType; healthy: boolean }>> {
const results: Array<{ type: CarrierType; healthy: boolean }> = [];
for (const [type, carrier] of this.carriers.entries()) {
const healthy = await carrier.isHealthy();
results.push({
type,
healthy,
});
}
return results;
}
clearCache(): void {
this.carriers.clear();
}
}