- 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>
111 lines
2.8 KiB
TypeScript
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();
|
|
}
|
|
}
|