This commit is contained in:
2025-05-08 12:46:10 +00:00
parent 7aaf8f2595
commit 8b857e3d1d
26 changed files with 5215 additions and 142 deletions

433
ts/config/base.config.ts Normal file
View File

@ -0,0 +1,433 @@
/**
* Base configuration interface with common properties for all services
*/
export interface IBaseConfig {
/**
* Unique identifier for this configuration
* Used to track configuration versions and changes
*/
id?: string;
/**
* Configuration version
* Used for migration between different config formats
*/
version?: string;
/**
* Environment this configuration is intended for
* (development, test, production, etc.)
*/
environment?: 'development' | 'test' | 'staging' | 'production';
/**
* Display name for this configuration
*/
name?: string;
/**
* Whether this configuration is enabled
* Services with disabled configuration shouldn't start
*/
enabled?: boolean;
/**
* Logging configuration
*/
logging?: {
/**
* Minimum log level to output
*/
level?: 'error' | 'warn' | 'info' | 'debug';
/**
* Whether to include structured data in logs
*/
structured?: boolean;
/**
* Whether to enable correlation tracking in logs
*/
correlationTracking?: boolean;
};
}
/**
* Base database configuration
*/
export interface IDatabaseConfig {
/**
* Database connection string or URL
*/
connectionString?: string;
/**
* Database host
*/
host?: string;
/**
* Database port
*/
port?: number;
/**
* Database name
*/
database?: string;
/**
* Database username
*/
username?: string;
/**
* Database password
*/
password?: string;
/**
* SSL configuration for database connection
*/
ssl?: boolean | {
/**
* Whether to reject unauthorized SSL connections
*/
rejectUnauthorized?: boolean;
/**
* Path to CA certificate file
*/
ca?: string;
/**
* Path to client certificate file
*/
cert?: string;
/**
* Path to client key file
*/
key?: string;
};
/**
* Connection pool configuration
*/
pool?: {
/**
* Minimum number of connections in pool
*/
min?: number;
/**
* Maximum number of connections in pool
*/
max?: number;
/**
* Connection idle timeout in milliseconds
*/
idleTimeoutMillis?: number;
};
}
/**
* Base TLS configuration interface
*/
export interface ITlsConfig {
/**
* Whether to enable TLS
*/
enabled?: boolean;
/**
* The domain name for the certificate
*/
domain?: string;
/**
* Path to certificate file
*/
certPath?: string;
/**
* Path to private key file
*/
keyPath?: string;
/**
* Path to CA certificate file
*/
caPath?: string;
/**
* Minimum TLS version to support
*/
minVersion?: 'TLSv1.2' | 'TLSv1.3';
/**
* Whether to auto-renew certificates
*/
autoRenew?: boolean;
/**
* Whether to reject unauthorized certificates
*/
rejectUnauthorized?: boolean;
}
/**
* Base retry configuration interface
*/
export interface IRetryConfig {
/**
* Maximum number of retry attempts
*/
maxAttempts?: number;
/**
* Base delay between retries in milliseconds
*/
baseDelay?: number;
/**
* Maximum delay between retries in milliseconds
*/
maxDelay?: number;
/**
* Backoff factor for exponential backoff
*/
backoffFactor?: number;
/**
* Specific error codes that should trigger retries
*/
retryableErrorCodes?: string[];
/**
* Whether to add jitter to retry delays
*/
useJitter?: boolean;
}
/**
* Base rate limiting configuration interface
*/
export interface IRateLimitConfig {
/**
* Whether rate limiting is enabled
*/
enabled?: boolean;
/**
* Maximum number of operations per period
*/
maxPerPeriod?: number;
/**
* Time period in milliseconds
*/
periodMs?: number;
/**
* Whether to apply per key (e.g., domain, user, etc.)
*/
perKey?: boolean;
/**
* Number of burst tokens allowed
*/
burstTokens?: number;
}
/**
* Basic HTTP server configuration
*/
export interface IHttpServerConfig {
/**
* Whether the HTTP server is enabled
*/
enabled?: boolean;
/**
* Host to bind to
*/
host?: string;
/**
* Port to listen on
*/
port?: number;
/**
* Path prefix for all routes
*/
basePath?: string;
/**
* CORS configuration
*/
cors?: boolean | {
/**
* Allowed origins
*/
origins?: string[];
/**
* Allowed methods
*/
methods?: string[];
/**
* Allowed headers
*/
headers?: string[];
/**
* Whether to allow credentials
*/
credentials?: boolean;
};
/**
* TLS configuration
*/
tls?: ITlsConfig;
/**
* Maximum request body size in bytes
*/
maxBodySize?: number;
/**
* Request timeout in milliseconds
*/
timeout?: number;
}
/**
* Basic queue configuration
*/
export interface IQueueConfig {
/**
* Type of storage for the queue
*/
storageType?: 'memory' | 'disk' | 'redis';
/**
* Path for persistent storage
*/
persistentPath?: string;
/**
* Redis configuration for queue
*/
redis?: {
/**
* Redis host
*/
host?: string;
/**
* Redis port
*/
port?: number;
/**
* Redis password
*/
password?: string;
/**
* Redis database number
*/
db?: number;
};
/**
* Maximum size of the queue
*/
maxSize?: number;
/**
* Maximum number of retry attempts
*/
maxRetries?: number;
/**
* Base delay between retries in milliseconds
*/
baseRetryDelay?: number;
/**
* Maximum delay between retries in milliseconds
*/
maxRetryDelay?: number;
/**
* Check interval for processing in milliseconds
*/
checkInterval?: number;
/**
* Maximum number of parallel processes
*/
maxParallelProcessing?: number;
}
/**
* Basic monitoring configuration
*/
export interface IMonitoringConfig {
/**
* Whether monitoring is enabled
*/
enabled?: boolean;
/**
* Metrics collection interval in milliseconds
*/
metricsInterval?: number;
/**
* Whether to expose Prometheus metrics
*/
exposePrometheus?: boolean;
/**
* Port for Prometheus metrics
*/
prometheusPort?: number;
/**
* Whether to collect detailed metrics
*/
detailedMetrics?: boolean;
/**
* Alert thresholds
*/
alertThresholds?: Record<string, number>;
/**
* Notification configuration
*/
notifications?: {
/**
* Whether to send notifications
*/
enabled?: boolean;
/**
* Email address to send notifications to
*/
email?: string;
/**
* Webhook URL to send notifications to
*/
webhook?: string;
};
}

266
ts/config/email.config.ts Normal file
View File

@ -0,0 +1,266 @@
import type { IBaseConfig, ITlsConfig, IQueueConfig, IRateLimitConfig, IMonitoringConfig } from './base.config.js';
/**
* Email service configuration
*/
export interface IEmailConfig extends IBaseConfig {
/**
* Whether to use MTA for sending emails
*/
useMta?: boolean;
/**
* MTA configuration
*/
mtaConfig?: IMtaConfig;
/**
* Template configuration
*/
templateConfig?: {
/**
* Default sender email address
*/
from?: string;
/**
* Default reply-to email address
*/
replyTo?: string;
/**
* Default footer HTML
*/
footerHtml?: string;
/**
* Default footer text
*/
footerText?: string;
};
/**
* Whether to load templates from directory
*/
loadTemplatesFromDir?: boolean;
/**
* Directory path for email templates
*/
templatesDir?: string;
}
/**
* MTA configuration
*/
export interface IMtaConfig {
/**
* SMTP server configuration
*/
smtp?: {
/**
* Whether to enable the SMTP server
*/
enabled?: boolean;
/**
* Port to listen on
*/
port?: number;
/**
* SMTP server hostname
*/
hostname?: string;
/**
* Maximum allowed email size in bytes
*/
maxSize?: number;
};
/**
* TLS configuration
*/
tls?: ITlsConfig;
/**
* Outbound email configuration
*/
outbound?: {
/**
* Maximum concurrent sending jobs
*/
concurrency?: number;
/**
* Retry configuration
*/
retries?: {
/**
* Maximum number of retries per message
*/
max?: number;
/**
* Initial delay between retries (milliseconds)
*/
delay?: number;
/**
* Whether to use exponential backoff for retries
*/
useBackoff?: boolean;
};
/**
* Rate limiting configuration
*/
rateLimit?: IRateLimitConfig;
/**
* IP warmup configuration
*/
warmup?: {
/**
* Whether IP warmup is enabled
*/
enabled?: boolean;
/**
* IP addresses to warm up
*/
ipAddresses?: string[];
/**
* Target domains to warm up
*/
targetDomains?: string[];
/**
* Allocation policy to use
*/
allocationPolicy?: string;
/**
* Fallback percentage for ESP routing during warmup
*/
fallbackPercentage?: number;
};
/**
* Reputation monitoring configuration
*/
reputation?: IMonitoringConfig & {
/**
* Alert thresholds
*/
alertThresholds?: {
/**
* Minimum acceptable reputation score
*/
minReputationScore?: number;
/**
* Maximum acceptable complaint rate
*/
maxComplaintRate?: number;
};
};
};
/**
* Security settings
*/
security?: {
/**
* Whether to use DKIM signing
*/
useDkim?: boolean;
/**
* Whether to verify inbound DKIM signatures
*/
verifyDkim?: boolean;
/**
* Whether to verify SPF on inbound
*/
verifySpf?: boolean;
/**
* Whether to verify DMARC on inbound
*/
verifyDmarc?: boolean;
/**
* Whether to enforce DMARC policy
*/
enforceDmarc?: boolean;
/**
* Whether to use TLS for outbound when available
*/
useTls?: boolean;
/**
* Whether to require valid certificates
*/
requireValidCerts?: boolean;
/**
* Log level for email security events
*/
securityLogLevel?: 'info' | 'warn' | 'error';
/**
* Whether to check IP reputation for inbound emails
*/
checkIPReputation?: boolean;
/**
* Whether to scan content for malicious payloads
*/
scanContent?: boolean;
/**
* Action to take when malicious content is detected
*/
maliciousContentAction?: 'tag' | 'quarantine' | 'reject';
/**
* Minimum threat score to trigger action
*/
threatScoreThreshold?: number;
/**
* Whether to reject connections from high-risk IPs
*/
rejectHighRiskIPs?: boolean;
};
/**
* Domains configuration
*/
domains?: {
/**
* List of domains that this MTA will handle as local
*/
local?: string[];
/**
* Whether to auto-create DNS records
*/
autoCreateDnsRecords?: boolean;
/**
* DKIM selector to use
*/
dkimSelector?: string;
};
/**
* Queue configuration
*/
queue?: IQueueConfig;
}

100
ts/config/index.ts Normal file
View File

@ -0,0 +1,100 @@
// Export configuration interfaces
export * from './base.config.js';
export * from './email.config.js';
export * from './sms.config.js';
export * from './platform.config.js';
// Export validation tools
export * from './validator.js';
export * from './schemas.js';
// Re-export commonly used types
import type { IPlatformConfig } from './platform.config.js';
import type { IEmailConfig, IMtaConfig } from './email.config.js';
import type { ISmsConfig } from './sms.config.js';
import type {
IBaseConfig,
ITlsConfig,
IHttpServerConfig,
IRateLimitConfig,
IQueueConfig
} from './base.config.js';
// Default platform configuration
export const defaultConfig: IPlatformConfig = {
id: 'platform-service-config',
version: '1.0.0',
environment: 'production',
name: 'PlatformService',
enabled: true,
logging: {
level: 'info',
structured: true,
correlationTracking: true
},
server: {
enabled: true,
host: '0.0.0.0',
port: 3000,
cors: true
},
email: {
useMta: true,
mtaConfig: {
smtp: {
enabled: true,
port: 25,
hostname: 'mta.lossless.one',
maxSize: 10 * 1024 * 1024 // 10MB
},
tls: {
domain: 'mta.lossless.one',
autoRenew: true
},
security: {
useDkim: true,
verifyDkim: true,
verifySpf: true,
verifyDmarc: true,
enforceDmarc: true,
useTls: true,
requireValidCerts: false,
securityLogLevel: 'warn',
checkIPReputation: true,
scanContent: true,
maliciousContentAction: 'tag',
threatScoreThreshold: 50,
rejectHighRiskIPs: false
},
domains: {
local: ['lossless.one'],
autoCreateDnsRecords: true,
dkimSelector: 'mta'
}
},
templateConfig: {
from: 'no-reply@lossless.one',
replyTo: 'support@lossless.one'
},
loadTemplatesFromDir: true
},
paths: {
dataDir: 'data',
logsDir: 'logs',
tempDir: 'temp',
emailTemplatesDir: 'templates/email'
}
};
// Export main types for convenience
export type {
IPlatformConfig,
IEmailConfig,
IMtaConfig,
ISmsConfig,
IBaseConfig,
ITlsConfig,
IHttpServerConfig,
IRateLimitConfig,
IQueueConfig
};

View File

@ -0,0 +1,54 @@
import type { IBaseConfig, IHttpServerConfig, IDatabaseConfig } from './base.config.js';
import type { IEmailConfig } from './email.config.js';
import type { ISmsConfig } from './sms.config.js';
/**
* Platform service configuration
* Root configuration that includes all service configurations
*/
export interface IPlatformConfig extends IBaseConfig {
/**
* HTTP server configuration
*/
server?: IHttpServerConfig;
/**
* Database configuration
*/
database?: IDatabaseConfig;
/**
* Email service configuration
*/
email?: IEmailConfig;
/**
* SMS service configuration
*/
sms?: ISmsConfig;
/**
* Path configuration
*/
paths?: {
/**
* Data directory path
*/
dataDir?: string;
/**
* Logs directory path
*/
logsDir?: string;
/**
* Temporary directory path
*/
tempDir?: string;
/**
* Email templates directory path
*/
emailTemplatesDir?: string;
};
}

770
ts/config/schemas.ts Normal file
View File

@ -0,0 +1,770 @@
import type { ValidationSchema } from './validator.js';
/**
* Base TLS configuration schema
*/
export const tlsConfigSchema: ValidationSchema = {
enabled: {
type: 'boolean',
required: false,
default: false
},
domain: {
type: 'string',
required: false
},
certPath: {
type: 'string',
required: false
},
keyPath: {
type: 'string',
required: false
},
caPath: {
type: 'string',
required: false
},
minVersion: {
type: 'string',
required: false,
enum: ['TLSv1.2', 'TLSv1.3'],
default: 'TLSv1.2'
},
autoRenew: {
type: 'boolean',
required: false,
default: false
},
rejectUnauthorized: {
type: 'boolean',
required: false,
default: true
}
};
/**
* HTTP server configuration schema
*/
export const httpServerSchema: ValidationSchema = {
enabled: {
type: 'boolean',
required: false,
default: true
},
host: {
type: 'string',
required: false,
default: '0.0.0.0'
},
port: {
type: 'number',
required: false,
default: 3000,
min: 1,
max: 65535
},
basePath: {
type: 'string',
required: false,
default: ''
},
cors: {
type: 'boolean',
required: false,
default: true
},
tls: {
type: 'object',
required: false,
schema: tlsConfigSchema
},
maxBodySize: {
type: 'number',
required: false,
default: 1024 * 1024 // 1MB
},
timeout: {
type: 'number',
required: false,
default: 30000 // 30 seconds
}
};
/**
* Rate limit configuration schema
*/
export const rateLimitSchema: ValidationSchema = {
enabled: {
type: 'boolean',
required: false,
default: true
},
maxPerPeriod: {
type: 'number',
required: false,
default: 100,
min: 1
},
periodMs: {
type: 'number',
required: false,
default: 60000, // 1 minute
min: 1000
},
perKey: {
type: 'boolean',
required: false,
default: true
},
burstTokens: {
type: 'number',
required: false,
default: 5,
min: 0
}
};
/**
* Queue configuration schema
*/
export const queueSchema: ValidationSchema = {
storageType: {
type: 'string',
required: false,
enum: ['memory', 'disk', 'redis'],
default: 'memory'
},
persistentPath: {
type: 'string',
required: false
},
redis: {
type: 'object',
required: false,
schema: {
host: {
type: 'string',
required: false,
default: 'localhost'
},
port: {
type: 'number',
required: false,
default: 6379,
min: 1,
max: 65535
},
password: {
type: 'string',
required: false
},
db: {
type: 'number',
required: false,
default: 0,
min: 0
}
}
},
maxSize: {
type: 'number',
required: false,
default: 10000,
min: 1
},
maxRetries: {
type: 'number',
required: false,
default: 3,
min: 0
},
baseRetryDelay: {
type: 'number',
required: false,
default: 1000, // 1 second
min: 1
},
maxRetryDelay: {
type: 'number',
required: false,
default: 60000, // 1 minute
min: 1
},
checkInterval: {
type: 'number',
required: false,
default: 1000, // 1 second
min: 100
},
maxParallelProcessing: {
type: 'number',
required: false,
default: 5,
min: 1
}
};
/**
* SMS service configuration schema
*/
export const smsConfigSchema: ValidationSchema = {
apiGatewayApiToken: {
type: 'string',
required: true
},
defaultSender: {
type: 'string',
required: false
},
rateLimit: {
type: 'object',
required: false,
schema: {
...rateLimitSchema,
maxPerRecipientPerDay: {
type: 'number',
required: false,
default: 10,
min: 1
}
}
},
provider: {
type: 'object',
required: false,
schema: {
type: {
type: 'string',
required: false,
enum: ['gateway', 'twilio', 'other'],
default: 'gateway'
},
config: {
type: 'object',
required: false
},
fallback: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: false
},
type: {
type: 'string',
required: false,
enum: ['gateway', 'twilio', 'other']
},
config: {
type: 'object',
required: false
}
}
}
}
},
verification: {
type: 'object',
required: false,
schema: {
codeLength: {
type: 'number',
required: false,
default: 6,
min: 4,
max: 10
},
expirationSeconds: {
type: 'number',
required: false,
default: 300, // 5 minutes
min: 60
},
maxAttempts: {
type: 'number',
required: false,
default: 3,
min: 1
},
cooldownSeconds: {
type: 'number',
required: false,
default: 60, // 1 minute
min: 0
}
}
}
};
/**
* MTA configuration schema
*/
export const mtaConfigSchema: ValidationSchema = {
smtp: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: true
},
port: {
type: 'number',
required: false,
default: 25,
min: 1,
max: 65535
},
hostname: {
type: 'string',
required: false,
default: 'mta.lossless.one'
},
maxSize: {
type: 'number',
required: false,
default: 10 * 1024 * 1024, // 10MB
min: 1024
}
}
},
tls: {
type: 'object',
required: false,
schema: tlsConfigSchema
},
outbound: {
type: 'object',
required: false,
schema: {
concurrency: {
type: 'number',
required: false,
default: 5,
min: 1
},
retries: {
type: 'object',
required: false,
schema: {
max: {
type: 'number',
required: false,
default: 3,
min: 0
},
delay: {
type: 'number',
required: false,
default: 300000, // 5 minutes
min: 1000
},
useBackoff: {
type: 'boolean',
required: false,
default: true
}
}
},
rateLimit: {
type: 'object',
required: false,
schema: rateLimitSchema
},
warmup: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: false
},
ipAddresses: {
type: 'array',
required: false,
items: {
type: 'string'
}
},
targetDomains: {
type: 'array',
required: false,
items: {
type: 'string'
}
},
allocationPolicy: {
type: 'string',
required: false,
default: 'balanced'
},
fallbackPercentage: {
type: 'number',
required: false,
default: 50,
min: 0,
max: 100
}
}
},
reputation: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: false
},
updateFrequency: {
type: 'number',
required: false,
default: 24 * 60 * 60 * 1000, // 1 day
min: 60000
},
alertThresholds: {
type: 'object',
required: false,
schema: {
minReputationScore: {
type: 'number',
required: false,
default: 70,
min: 0,
max: 100
},
maxComplaintRate: {
type: 'number',
required: false,
default: 0.1, // 0.1%
min: 0,
max: 100
}
}
}
}
}
}
},
security: {
type: 'object',
required: false,
schema: {
useDkim: {
type: 'boolean',
required: false,
default: true
},
verifyDkim: {
type: 'boolean',
required: false,
default: true
},
verifySpf: {
type: 'boolean',
required: false,
default: true
},
verifyDmarc: {
type: 'boolean',
required: false,
default: true
},
enforceDmarc: {
type: 'boolean',
required: false,
default: true
},
useTls: {
type: 'boolean',
required: false,
default: true
},
requireValidCerts: {
type: 'boolean',
required: false,
default: false
},
securityLogLevel: {
type: 'string',
required: false,
enum: ['info', 'warn', 'error'],
default: 'warn'
},
checkIPReputation: {
type: 'boolean',
required: false,
default: true
},
scanContent: {
type: 'boolean',
required: false,
default: true
},
maliciousContentAction: {
type: 'string',
required: false,
enum: ['tag', 'quarantine', 'reject'],
default: 'tag'
},
threatScoreThreshold: {
type: 'number',
required: false,
default: 50,
min: 0,
max: 100
},
rejectHighRiskIPs: {
type: 'boolean',
required: false,
default: false
}
}
},
domains: {
type: 'object',
required: false,
schema: {
local: {
type: 'array',
required: false,
items: {
type: 'string'
},
default: ['lossless.one']
},
autoCreateDnsRecords: {
type: 'boolean',
required: false,
default: true
},
dkimSelector: {
type: 'string',
required: false,
default: 'mta'
}
}
},
queue: {
type: 'object',
required: false,
schema: queueSchema
}
};
/**
* Email service configuration schema
*/
export const emailConfigSchema: ValidationSchema = {
useMta: {
type: 'boolean',
required: false,
default: true
},
mtaConfig: {
type: 'object',
required: false,
schema: mtaConfigSchema
},
templateConfig: {
type: 'object',
required: false,
schema: {
from: {
type: 'string',
required: false,
default: 'no-reply@lossless.one'
},
replyTo: {
type: 'string',
required: false,
default: 'support@lossless.one'
},
footerHtml: {
type: 'string',
required: false
},
footerText: {
type: 'string',
required: false
}
}
},
loadTemplatesFromDir: {
type: 'boolean',
required: false,
default: true
},
templatesDir: {
type: 'string',
required: false
}
};
/**
* Database configuration schema
*/
export const databaseConfigSchema: ValidationSchema = {
connectionString: {
type: 'string',
required: false
},
host: {
type: 'string',
required: false,
default: 'localhost'
},
port: {
type: 'number',
required: false,
default: 5432,
min: 1,
max: 65535
},
database: {
type: 'string',
required: false
},
username: {
type: 'string',
required: false
},
password: {
type: 'string',
required: false
},
ssl: {
type: 'boolean',
required: false,
default: false
},
pool: {
type: 'object',
required: false,
schema: {
min: {
type: 'number',
required: false,
default: 2,
min: 1
},
max: {
type: 'number',
required: false,
default: 10,
min: 1
},
idleTimeoutMillis: {
type: 'number',
required: false,
default: 30000,
min: 1000
}
}
}
};
/**
* Platform service configuration schema
*/
export const platformConfigSchema: ValidationSchema = {
id: {
type: 'string',
required: false,
default: 'platform-service-config'
},
version: {
type: 'string',
required: false,
default: '1.0.0'
},
environment: {
type: 'string',
required: false,
enum: ['development', 'test', 'staging', 'production'],
default: 'production'
},
name: {
type: 'string',
required: false,
default: 'PlatformService'
},
enabled: {
type: 'boolean',
required: false,
default: true
},
logging: {
type: 'object',
required: false,
schema: {
level: {
type: 'string',
required: false,
enum: ['error', 'warn', 'info', 'debug'],
default: 'info'
},
structured: {
type: 'boolean',
required: false,
default: true
},
correlationTracking: {
type: 'boolean',
required: false,
default: true
}
}
},
server: {
type: 'object',
required: false,
schema: httpServerSchema
},
database: {
type: 'object',
required: false,
schema: databaseConfigSchema
},
email: {
type: 'object',
required: false,
schema: emailConfigSchema
},
sms: {
type: 'object',
required: false,
schema: smsConfigSchema
},
paths: {
type: 'object',
required: false,
schema: {
dataDir: {
type: 'string',
required: false,
default: 'data'
},
logsDir: {
type: 'string',
required: false,
default: 'logs'
},
tempDir: {
type: 'string',
required: false,
default: 'temp'
},
emailTemplatesDir: {
type: 'string',
required: false,
default: 'templates/email'
}
}
}
};

86
ts/config/sms.config.ts Normal file
View File

@ -0,0 +1,86 @@
import type { IBaseConfig, IRateLimitConfig } from './base.config.js';
/**
* SMS service configuration
*/
export interface ISmsConfig extends IBaseConfig {
/**
* API token for the gateway service
*/
apiGatewayApiToken: string;
/**
* Default sender ID or phone number
*/
defaultSender?: string;
/**
* SMS rate limiting
*/
rateLimit?: IRateLimitConfig & {
/**
* Maximum messages per recipient per day
*/
maxPerRecipientPerDay?: number;
};
/**
* SMS provider configuration
*/
provider?: {
/**
* Provider type
*/
type?: 'gateway' | 'twilio' | 'other';
/**
* Provider-specific configuration
*/
config?: Record<string, any>;
/**
* Fallback provider configuration
*/
fallback?: {
/**
* Whether to use fallback provider
*/
enabled?: boolean;
/**
* Provider type
*/
type?: 'gateway' | 'twilio' | 'other';
/**
* Provider-specific configuration
*/
config?: Record<string, any>;
};
};
/**
* Verification code settings
*/
verification?: {
/**
* Code length
*/
codeLength?: number;
/**
* Code expiration time in seconds
*/
expirationSeconds?: number;
/**
* Maximum number of attempts
*/
maxAttempts?: number;
/**
* Cooldown period in seconds
*/
cooldownSeconds?: number;
};
}

326
ts/config/validator.ts Normal file
View File

@ -0,0 +1,326 @@
import * as plugins from '../plugins.js';
import { ValidationError } from '../errors/base.errors.js';
import type { IBaseConfig } from './base.config.js';
/**
* Validation result
*/
export interface IValidationResult {
/**
* Whether the validation passed
*/
valid: boolean;
/**
* Validation errors if any
*/
errors?: string[];
/**
* Validated configuration (may include defaults)
*/
config?: any;
}
/**
* Validation schema types
*/
export type ValidationSchema = Record<string, {
/**
* Type of the value
*/
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
/**
* Whether the field is required
*/
required?: boolean;
/**
* Default value if not specified
*/
default?: any;
/**
* Minimum value (for numbers)
*/
min?: number;
/**
* Maximum value (for numbers)
*/
max?: number;
/**
* Minimum length (for strings or arrays)
*/
minLength?: number;
/**
* Maximum length (for strings or arrays)
*/
maxLength?: number;
/**
* Pattern to match (for strings)
*/
pattern?: RegExp;
/**
* Allowed values (for strings, numbers)
*/
enum?: any[];
/**
* Nested schema (for objects)
*/
schema?: ValidationSchema;
/**
* Item schema (for arrays)
*/
items?: {
type: 'string' | 'number' | 'boolean' | 'object';
schema?: ValidationSchema;
};
/**
* Custom validation function
*/
validate?: (value: any) => boolean | string;
}>;
/**
* Configuration validator
* Validates configuration objects against schemas and provides default values
*/
export class ConfigValidator {
/**
* Basic schema for IBaseConfig
*/
private static baseConfigSchema: ValidationSchema = {
id: {
type: 'string',
required: false
},
version: {
type: 'string',
required: false
},
environment: {
type: 'string',
required: false,
enum: ['development', 'test', 'staging', 'production'],
default: 'production'
},
name: {
type: 'string',
required: false
},
enabled: {
type: 'boolean',
required: false,
default: true
},
logging: {
type: 'object',
required: false,
schema: {
level: {
type: 'string',
required: false,
enum: ['error', 'warn', 'info', 'debug'],
default: 'info'
},
structured: {
type: 'boolean',
required: false,
default: true
},
correlationTracking: {
type: 'boolean',
required: false,
default: true
}
}
}
};
/**
* Validate a configuration object against a schema
*
* @param config Configuration object to validate
* @param schema Validation schema
* @returns Validation result
*/
public static validate<T>(config: T, schema: ValidationSchema): IValidationResult {
const errors: string[] = [];
const validatedConfig = { ...config };
// Validate each field against the schema
for (const [key, rules] of Object.entries(schema)) {
const value = config[key];
// Check if required
if (rules.required && (value === undefined || value === null)) {
errors.push(`${key} is required`);
continue;
}
// If not present and not required, apply default if available
if ((value === undefined || value === null)) {
if (rules.default !== undefined) {
validatedConfig[key] = rules.default;
}
continue;
}
// Type validation
if (value !== undefined && value !== null) {
const valueType = Array.isArray(value) ? 'array' : typeof value;
if (valueType !== rules.type) {
errors.push(`${key} must be of type ${rules.type}, got ${valueType}`);
continue;
}
// Type-specific validations
switch (rules.type) {
case 'number':
if (rules.min !== undefined && value < rules.min) {
errors.push(`${key} must be at least ${rules.min}`);
}
if (rules.max !== undefined && value > rules.max) {
errors.push(`${key} must be at most ${rules.max}`);
}
break;
case 'string':
if (rules.minLength !== undefined && value.length < rules.minLength) {
errors.push(`${key} must be at least ${rules.minLength} characters`);
}
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
errors.push(`${key} must be at most ${rules.maxLength} characters`);
}
if (rules.pattern && !rules.pattern.test(value)) {
errors.push(`${key} must match pattern ${rules.pattern}`);
}
break;
case 'array':
if (rules.minLength !== undefined && value.length < rules.minLength) {
errors.push(`${key} must have at least ${rules.minLength} items`);
}
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
errors.push(`${key} must have at most ${rules.maxLength} items`);
}
if (rules.items && value.length > 0) {
for (let i = 0; i < value.length; i++) {
const itemType = Array.isArray(value[i]) ? 'array' : typeof value[i];
if (itemType !== rules.items.type) {
errors.push(`${key}[${i}] must be of type ${rules.items.type}, got ${itemType}`);
} else if (rules.items.schema && itemType === 'object') {
const itemResult = this.validate(value[i], rules.items.schema);
if (!itemResult.valid) {
errors.push(...itemResult.errors.map(err => `${key}[${i}].${err}`));
}
}
}
}
break;
case 'object':
if (rules.schema) {
const nestedResult = this.validate(value, rules.schema);
if (!nestedResult.valid) {
errors.push(...nestedResult.errors.map(err => `${key}.${err}`));
}
validatedConfig[key] = nestedResult.config;
}
break;
}
// Enum validation
if (rules.enum && !rules.enum.includes(value)) {
errors.push(`${key} must be one of [${rules.enum.join(', ')}]`);
}
// Custom validation
if (rules.validate) {
const result = rules.validate(value);
if (result !== true) {
errors.push(typeof result === 'string' ? result : `${key} failed custom validation`);
}
}
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
config: validatedConfig
};
}
/**
* Validate base configuration
*
* @param config Base configuration
* @returns Validation result for base configuration
*/
public static validateBaseConfig(config: IBaseConfig): IValidationResult {
return this.validate(config, this.baseConfigSchema);
}
/**
* Apply defaults to a configuration object based on a schema
*
* @param config Configuration object to apply defaults to
* @param schema Validation schema with defaults
* @returns Configuration with defaults applied
*/
public static applyDefaults<T>(config: T, schema: ValidationSchema): T {
const result = { ...config };
for (const [key, rules] of Object.entries(schema)) {
if (result[key] === undefined && rules.default !== undefined) {
result[key] = rules.default;
}
// Apply defaults to nested objects
if (result[key] && rules.type === 'object' && rules.schema) {
result[key] = this.applyDefaults(result[key], rules.schema);
}
// Apply defaults to array items
if (result[key] && rules.type === 'array' && rules.items && rules.items.schema) {
result[key] = result[key].map(item =>
typeof item === 'object' ? this.applyDefaults(item, rules.items.schema) : item
);
}
}
return result;
}
/**
* Throw a validation error if the configuration is invalid
*
* @param config Configuration to validate
* @param schema Validation schema
* @returns Validated configuration with defaults
* @throws ValidationError if validation fails
*/
public static validateOrThrow<T>(config: T, schema: ValidationSchema): T {
const result = this.validate(config, schema);
if (!result.valid) {
throw new ValidationError(
`Configuration validation failed: ${result.errors.join(', ')}`,
'CONFIG_VALIDATION_ERROR',
{ data: { errors: result.errors } }
);
}
return result.config;
}
}

437
ts/errors/base.errors.ts Normal file
View File

@ -0,0 +1,437 @@
import { ErrorSeverity, ErrorCategory, ErrorRecoverability } from './error.codes.js';
import { logger } from '../logger.js';
// Import TLogLevel from plugins
import type { TLogLevel } from '../plugins.js';
/**
* Context information added to structured errors
*/
export interface IErrorContext {
/** Component or service where the error occurred */
component?: string;
/** Operation that was being performed */
operation?: string;
/** Unique request ID if available */
requestId?: string;
/** Error occurred at timestamp */
timestamp?: number;
/** User-visible message (safe to display to end-users) */
userMessage?: string;
/** Additional structured data for debugging */
data?: Record<string, any>;
/** Related entity IDs if applicable */
entity?: {
type: string;
id: string | number;
};
/** Stack trace (if enabled in configuration) */
stack?: string;
/** Retry information if applicable */
retry?: {
/** Maximum number of retries allowed */
maxRetries?: number;
/** Current retry count */
currentRetry?: number;
/** Next retry timestamp */
nextRetryAt?: number;
/** Delay between retries (in ms) */
retryDelay?: number;
};
}
/**
* Base class for all errors in the Platform Service
* Adds structured error information, logging, and error tracking
*/
export class PlatformError extends Error {
/** Error code identifying the specific error type */
public readonly code: string;
/** Error severity level */
public readonly severity: ErrorSeverity;
/** Error category for grouping related errors */
public readonly category: ErrorCategory;
/** Whether the error can be recovered from automatically */
public readonly recoverability: ErrorRecoverability;
/** Additional context information */
public readonly context: IErrorContext;
/**
* Creates a new PlatformError
*
* @param message Error message
* @param code Error code from error.codes.ts
* @param severity Error severity level
* @param category Error category
* @param recoverability Error recoverability indication
* @param context Additional context information
*/
constructor(
message: string,
code: string,
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
category: ErrorCategory = ErrorCategory.OTHER,
recoverability: ErrorRecoverability = ErrorRecoverability.NON_RECOVERABLE,
context: IErrorContext = {}
) {
super(message);
// Set error metadata
this.name = this.constructor.name;
this.code = code;
this.severity = severity;
this.category = category;
this.recoverability = recoverability;
// Add timestamp if not provided
this.context = {
...context,
timestamp: context.timestamp || Date.now(),
};
// Capture stack trace
Error.captureStackTrace(this, this.constructor);
// Log the error automatically unless explicitly disabled
if (!context.data?.skipLogging) {
this.logError();
}
}
/**
* Logs the error using the platform logger
*/
private logError(): void {
const logLevel = this.getLogLevelFromSeverity() as TLogLevel;
// Construct structured log entry
const logData = {
error_code: this.code,
error_name: this.name,
severity: this.severity,
category: this.category,
recoverability: this.recoverability,
...this.context
};
// Log with appropriate level
logger.log(logLevel, this.message, logData);
}
/**
* Maps severity levels to log levels
*/
private getLogLevelFromSeverity(): string {
switch (this.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
return 'error';
case ErrorSeverity.MEDIUM:
return 'warn';
case ErrorSeverity.LOW:
return 'info';
case ErrorSeverity.INFO:
return 'debug';
default:
return 'error';
}
}
/**
* Returns a JSON representation of the error
*/
public toJSON(): Record<string, any> {
return {
name: this.name,
message: this.message,
code: this.code,
severity: this.severity,
category: this.category,
recoverability: this.recoverability,
context: this.context,
stack: process.env.NODE_ENV !== 'production' ? this.stack : undefined
};
}
/**
* Creates an instance with retry information
*
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
*/
public withRetry(
maxRetries: number,
currentRetry: number = 0,
retryDelay: number = 1000
): PlatformError {
const nextRetryAt = Date.now() + retryDelay;
// Create a new instance with the same parameters but updated context
return new (this.constructor as typeof PlatformError)(
this.message,
this.code,
this.severity,
this.category,
// If we can retry, the error is at least maybe recoverable
currentRetry < maxRetries
? ErrorRecoverability.MAYBE_RECOVERABLE
: this.recoverability,
{
...this.context,
retry: {
maxRetries,
currentRetry,
nextRetryAt,
retryDelay
}
}
);
}
/**
* Checks if the error should be retried based on retry information
*/
public shouldRetry(): boolean {
const { retry } = this.context;
if (!retry) return false;
return retry.currentRetry < retry.maxRetries;
}
/**
* Returns a user-friendly message that is safe to display to end users
*/
public getUserMessage(): string {
return this.context.userMessage || 'An unexpected error occurred.';
}
}
/**
* Error class for validation errors
*/
export class ValidationError extends PlatformError {
/**
* Creates a new validation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.LOW,
ErrorCategory.VALIDATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for configuration errors
*/
export class ConfigurationError extends PlatformError {
/**
* Creates a new configuration error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.CONFIGURATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for network-related errors
*/
export class NetworkError extends PlatformError {
/**
* Creates a new network error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.CONNECTIVITY,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for resource availability errors (rate limits, quotas)
*/
export class ResourceError extends PlatformError {
/**
* Creates a new resource error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.RESOURCE,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for authentication/authorization errors
*/
export class AuthenticationError extends PlatformError {
/**
* Creates a new authentication error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.HIGH,
ErrorCategory.AUTHENTICATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for operation errors (API calls, processing)
*/
export class OperationError extends PlatformError {
/**
* Creates a new operation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for critical system errors
*/
export class SystemError extends PlatformError {
/**
* Creates a new system error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.CRITICAL,
ErrorCategory.OTHER,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Helper to get the appropriate error class based on error category
*
* @param category Error category
* @returns The appropriate error class
*/
export function getErrorClassForCategory(category: ErrorCategory): any {
switch (category) {
case ErrorCategory.VALIDATION:
return ValidationError;
case ErrorCategory.CONFIGURATION:
return ConfigurationError;
case ErrorCategory.CONNECTIVITY:
return NetworkError;
case ErrorCategory.RESOURCE:
return ResourceError;
case ErrorCategory.AUTHENTICATION:
return AuthenticationError;
case ErrorCategory.OPERATION:
return OperationError;
default:
return PlatformError;
}
}

313
ts/errors/email.errors.ts Normal file
View File

@ -0,0 +1,313 @@
import {
PlatformError,
ValidationError,
NetworkError,
ResourceError,
OperationError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
EMAIL_SERVICE_ERROR,
EMAIL_TEMPLATE_ERROR,
EMAIL_VALIDATION_ERROR,
EMAIL_SEND_ERROR,
EMAIL_RECEIVE_ERROR,
EMAIL_ATTACHMENT_ERROR,
EMAIL_PARSE_ERROR,
EMAIL_RATE_LIMIT_EXCEEDED
} from './error.codes.js';
/**
* Base class for all email service related errors
*/
export class EmailServiceError extends OperationError {
/**
* Creates a new email service error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_SERVICE_ERROR, context);
}
}
/**
* Error class for email template errors
*/
export class EmailTemplateError extends OperationError {
/**
* Creates a new email template error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_TEMPLATE_ERROR, context);
}
}
/**
* Error class for email validation errors
*/
export class EmailValidationError extends ValidationError {
/**
* Creates a new email validation error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_VALIDATION_ERROR, context);
}
}
/**
* Error class for email sending errors
*/
export class EmailSendError extends OperationError {
/**
* Creates a new email send error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_SEND_ERROR, context);
}
/**
* Creates an instance for a permanently failed send
*
* @param message Error message
* @param context Additional context
*/
public static permanent(
message: string,
context: IErrorContext = {}
): EmailSendError {
return new EmailSendError(`Permanent send failure: ${message}`, {
...context,
data: {
...context.data,
permanent: true
},
userMessage: 'The email could not be delivered due to a permanent failure.'
});
}
/**
* Creates an instance for a temporary failed send
*
* @param message Error message
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
* @param context Additional context
*/
public static temporary(
message: string,
maxRetries: number = 3,
currentRetry: number = 0,
retryDelay: number = 60000,
context: IErrorContext = {}
): EmailSendError {
const error = new EmailSendError(`Temporary send failure: ${message}`, {
...context,
data: {
...context.data,
permanent: false
},
userMessage: 'The email delivery failed temporarily. It will be retried.'
});
return error.withRetry(maxRetries, currentRetry, retryDelay) as EmailSendError;
}
/**
* Check if this is a permanent send failure
*/
public isPermanent(): boolean {
return !!this.context.data?.permanent;
}
}
/**
* Error class for email receiving errors
*/
export class EmailReceiveError extends OperationError {
/**
* Creates a new email receive error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_RECEIVE_ERROR, context);
}
}
/**
* Error class for email attachment errors
*/
export class EmailAttachmentError extends ValidationError {
/**
* Creates a new email attachment error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_ATTACHMENT_ERROR, context);
}
/**
* Creates an instance for an attachment too large error
*
* @param size Attachment size in bytes
* @param maxSize Maximum allowed size in bytes
* @param filename Attachment filename
* @param context Additional context
*/
public static tooLarge(
size: number,
maxSize: number,
filename?: string,
context: IErrorContext = {}
): EmailAttachmentError {
const filenameText = filename ? ` (${filename})` : '';
return new EmailAttachmentError(
`Attachment${filenameText} size ${size} bytes exceeds maximum allowed size of ${maxSize} bytes`,
{
...context,
data: {
...context.data,
size,
maxSize,
filename
},
userMessage: `The attachment${filenameText} is too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)} MB.`
}
);
}
/**
* Creates an instance for an invalid attachment type error
*
* @param contentType Attachment content type
* @param filename Attachment filename
* @param allowedTypes List of allowed content types
* @param context Additional context
*/
public static invalidType(
contentType: string,
filename: string,
allowedTypes: string[],
context: IErrorContext = {}
): EmailAttachmentError {
return new EmailAttachmentError(
`Attachment '${filename}' with content type '${contentType}' is not allowed. Allowed types: ${allowedTypes.join(', ')}`,
{
...context,
data: {
...context.data,
contentType,
filename,
allowedTypes
},
userMessage: `The attachment type ${contentType} is not allowed.`
}
);
}
}
/**
* Error class for email parsing errors
*/
export class EmailParseError extends OperationError {
/**
* Creates a new email parse error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_PARSE_ERROR, context);
}
}
/**
* Error class for email rate limit exceeded errors
*/
export class EmailRateLimitError extends ResourceError {
/**
* Creates a new email rate limit error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_RATE_LIMIT_EXCEEDED, context);
}
/**
* Creates an instance with rate limit information
*
* @param limit Rate limit
* @param remaining Remaining quota
* @param resetAt Time when the quota resets
* @param scope Rate limit scope (global, domain, user, etc.)
* @param context Additional context
*/
public static withLimitInfo(
limit: number,
remaining: number,
resetAt: Date | number,
scope: string = 'global',
context: IErrorContext = {}
): EmailRateLimitError {
const resetTime = typeof resetAt === 'number' ? new Date(resetAt) : resetAt;
const resetTimeStr = resetTime.toISOString();
return new EmailRateLimitError(
`Email rate limit exceeded: ${remaining}/${limit} remaining in ${scope} scope, resets at ${resetTimeStr}`,
{
...context,
data: {
...context.data,
limit,
remaining,
resetAt: resetTime.getTime(),
resetTimeStr,
scope
},
userMessage: `You've reached the email sending limit. Please try again later.`
}
);
}
}

412
ts/errors/error-handler.ts Normal file
View File

@ -0,0 +1,412 @@
import { PlatformError } from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import { ErrorCategory, ErrorRecoverability, ErrorSeverity } from './error.codes.js';
import { logger } from '../logger.js';
/**
* Error handler configuration
*/
export interface IErrorHandlerConfig {
/** Whether to log errors automatically */
logErrors: boolean;
/** Whether to include stack traces in prod environment */
includeStacksInProd: boolean;
/** Default retry options */
retry: {
/** Maximum retry attempts */
maxAttempts: number;
/** Base delay between retries in ms */
baseDelay: number;
/** Maximum delay between retries in ms */
maxDelay: number;
/** Backoff factor for exponential backoff */
backoffFactor: number;
};
}
/**
* Global error handler configuration
*/
const config: IErrorHandlerConfig = {
logErrors: true,
includeStacksInProd: false,
retry: {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 30000,
backoffFactor: 2
}
};
/**
* Error handler utility
* Provides methods for consistent error handling across the platform
*/
export class ErrorHandler {
/**
* Current configuration
*/
public static config = config;
/**
* Update error handler configuration
*
* @param newConfig New configuration (partial)
*/
public static configure(newConfig: Partial<IErrorHandlerConfig>): void {
ErrorHandler.config = {
...ErrorHandler.config,
...newConfig,
retry: {
...ErrorHandler.config.retry,
...(newConfig.retry || {})
}
};
}
/**
* Convert any error to a PlatformError
*
* @param error Error to convert
* @param defaultCode Default error code if not a PlatformError
* @param context Additional context
* @returns PlatformError instance
*/
public static toPlatformError(
error: any,
defaultCode: string,
context: IErrorContext = {}
): PlatformError {
// If already a PlatformError, just add context
if (error instanceof PlatformError) {
// Add context if provided
if (Object.keys(context).length > 0) {
return new (error.constructor as typeof PlatformError)(
error.message,
error.code,
error.severity,
error.category,
error.recoverability,
{
...error.context,
...context,
data: {
...(error.context.data || {}),
...(context.data || {})
}
}
);
}
return error;
}
// Convert standard Error to PlatformError
if (error instanceof Error) {
return new PlatformError(
error.message,
defaultCode,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.NON_RECOVERABLE,
{
...context,
data: {
...(context.data || {}),
originalError: {
name: error.name,
message: error.message,
stack: error.stack
}
}
}
);
}
// Not an Error instance
return new PlatformError(
typeof error === 'string' ? error : 'Unknown error',
defaultCode,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
/**
* Format an error for API responses
* Sanitizes errors for safe external exposure
*
* @param error Error to format
* @param includeDetails Whether to include detailed information
* @returns Formatted error object
*/
public static formatErrorForResponse(
error: any,
includeDetails: boolean = false
): Record<string, any> {
const platformError = ErrorHandler.toPlatformError(
error,
'PLATFORM_OPERATION_ERROR'
);
// Basic error information
const responseError: Record<string, any> = {
code: platformError.code,
message: platformError.getUserMessage(),
requestId: platformError.context.requestId
};
// Include more details if requested
if (includeDetails) {
responseError.details = {
severity: platformError.severity,
category: platformError.category,
rawMessage: platformError.message,
data: platformError.context.data
};
// Only include stack trace in non-production or if explicitly enabled
if (process.env.NODE_ENV !== 'production' || ErrorHandler.config.includeStacksInProd) {
responseError.details.stack = platformError.stack;
}
}
return responseError;
}
/**
* Handle an error with consistent logging and formatting
*
* @param error Error to handle
* @param defaultCode Default error code if not a PlatformError
* @param context Additional context
* @returns Formatted error for response
*/
public static handleError(
error: any,
defaultCode: string,
context: IErrorContext = {}
): Record<string, any> {
const platformError = ErrorHandler.toPlatformError(
error,
defaultCode,
context
);
// Log the error if enabled
if (ErrorHandler.config.logErrors) {
logger.error(platformError.message, {
error_code: platformError.code,
error_name: platformError.name,
error_severity: platformError.severity,
error_category: platformError.category,
error_recoverability: platformError.recoverability,
...platformError.context,
stack: platformError.stack
});
}
// Return formatted error for response
const isDetailedMode = process.env.NODE_ENV !== 'production';
return ErrorHandler.formatErrorForResponse(platformError, isDetailedMode);
}
/**
* Execute a function with error handling
*
* @param fn Function to execute
* @param defaultCode Default error code if the function throws
* @param context Additional context
* @returns Function result or error
*/
public static async execute<T>(
fn: () => Promise<T>,
defaultCode: string,
context: IErrorContext = {}
): Promise<T> {
try {
return await fn();
} catch (error) {
throw ErrorHandler.toPlatformError(error, defaultCode, context);
}
}
/**
* Execute a function with retries and exponential backoff
*
* @param fn Function to execute
* @param defaultCode Default error code if the function throws
* @param options Retry options
* @param context Additional context
* @returns Function result or error after max retries
*/
public static async executeWithRetry<T>(
fn: () => Promise<T>,
defaultCode: string,
options: {
maxAttempts?: number;
baseDelay?: number;
maxDelay?: number;
backoffFactor?: number;
retryableErrorCodes?: string[];
retryableErrorPatterns?: RegExp[];
onRetry?: (error: PlatformError, attempt: number, delay: number) => void;
} = {},
context: IErrorContext = {}
): Promise<T> {
const {
maxAttempts = ErrorHandler.config.retry.maxAttempts,
baseDelay = ErrorHandler.config.retry.baseDelay,
maxDelay = ErrorHandler.config.retry.maxDelay,
backoffFactor = ErrorHandler.config.retry.backoffFactor,
retryableErrorCodes = [],
retryableErrorPatterns = [],
onRetry = () => {}
} = options;
let lastError: PlatformError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
// Convert to PlatformError
const platformError = ErrorHandler.toPlatformError(
error,
defaultCode,
{
...context,
retry: {
currentRetry: attempt,
maxRetries: maxAttempts,
nextRetryAt: 0 // Will be set below if retrying
}
}
);
lastError = platformError;
// Check if we should retry
const isLastAttempt = attempt >= maxAttempts - 1;
if (isLastAttempt) {
// No more retries
throw platformError;
}
// Check if error is retryable
const isRetryable =
// Built-in recoverability
platformError.recoverability === ErrorRecoverability.RECOVERABLE ||
platformError.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE ||
platformError.recoverability === ErrorRecoverability.TRANSIENT ||
// Specifically included error codes
retryableErrorCodes.includes(platformError.code) ||
// Matches error message patterns
retryableErrorPatterns.some(pattern => pattern.test(platformError.message));
if (!isRetryable) {
throw platformError;
}
// Calculate delay with exponential backoff
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
// Add jitter to prevent thundering herd problem (±20%)
const jitter = 0.8 + Math.random() * 0.4;
const actualDelay = Math.floor(delay * jitter);
// Update nextRetryAt in error context
const nextRetryAt = Date.now() + actualDelay;
platformError.context.retry!.nextRetryAt = nextRetryAt;
// Log retry attempt
logger.warn(`Retrying operation after error (attempt ${attempt + 1}/${maxAttempts}): ${platformError.message}`, {
error_code: platformError.code,
retry_attempt: attempt + 1,
retry_max_attempts: maxAttempts,
retry_delay_ms: actualDelay,
retry_next_at: new Date(nextRetryAt).toISOString()
});
// Call onRetry callback
onRetry(platformError, attempt + 1, actualDelay);
// Wait before next retry
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
}
// This should never happen, but TypeScript needs it
throw lastError!;
}
}
/**
* Create a middleware for handling errors in HTTP requests
*
* @returns Middleware function
*/
export function createErrorHandlerMiddleware() {
return (error: any, req: any, res: any, next: any) => {
// Add request context
const context: IErrorContext = {
requestId: req.headers['x-request-id'] || req.headers['x-correlation-id'],
component: 'HttpServer',
operation: `${req.method} ${req.url}`,
data: {
method: req.method,
url: req.url,
query: req.query,
params: req.params,
ip: req.ip || req.connection.remoteAddress,
userAgent: req.headers['user-agent']
}
};
// Handle the error
const formattedError = ErrorHandler.handleError(
error,
'PLATFORM_OPERATION_ERROR',
context
);
// Set status code based on error type
let statusCode = 500;
if (error instanceof PlatformError) {
// Map error categories to HTTP status codes
switch (error.category) {
case ErrorCategory.VALIDATION:
statusCode = 400;
break;
case ErrorCategory.AUTHENTICATION:
statusCode = 401;
break;
case ErrorCategory.RESOURCE:
statusCode = 429;
break;
case ErrorCategory.OPERATION:
statusCode = 400;
break;
default:
statusCode = 500;
}
} else if (error.statusCode) {
// Use provided status code if available
statusCode = error.statusCode;
}
// Send error response
res.status(statusCode).json({
success: false,
error: formattedError
});
};
}

193
ts/errors/index.ts Normal file
View File

@ -0,0 +1,193 @@
/**
* Platform Service Error System
*
* This module provides a comprehensive error handling system for the Platform Service,
* with structured error types, error codes, and consistent patterns for logging and recovery.
*/
// Export error codes and types
export * from './error.codes.js';
// Export base error classes
export * from './base.errors.js';
// Export domain-specific error classes
export * from './email.errors.js';
export * from './mta.errors.js';
export * from './reputation.errors.js';
// Export utility function to create specific error types based on the error category
import { getErrorClassForCategory } from './base.errors.js';
export { getErrorClassForCategory };
/**
* Create a typed error from a standard Error
* Useful for converting errors from external libraries or APIs
*
* @param error Standard error to convert
* @param code Error code to assign
* @param contextData Additional context data
* @returns Typed PlatformError
*/
export function fromError(
error: Error,
code: string,
contextData: Record<string, any> = {}
) {
// Import and use PlatformError
const { PlatformError } = require('./base.errors.js');
const { ErrorSeverity, ErrorCategory, ErrorRecoverability } = require('./error.codes.js');
return new PlatformError(
error.message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.NON_RECOVERABLE,
{
data: {
...contextData,
originalError: {
name: error.name,
message: error.message,
stack: error.stack
}
}
}
);
}
/**
* Determine if an error is retryable
*
* @param error Error to check
* @returns Boolean indicating if the error should be retried
*/
export function isRetryable(error: any): boolean {
// If it's our platform error, use its recoverability property
if (error && typeof error === 'object' && 'recoverability' in error) {
const { ErrorRecoverability } = require('./error.codes.js');
return error.recoverability === ErrorRecoverability.RECOVERABLE ||
error.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE ||
error.recoverability === ErrorRecoverability.TRANSIENT;
}
// Check if it's a network error (these are often transient)
if (error && typeof error === 'object' && error.code) {
const networkErrors = [
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EHOSTUNREACH',
'ENETUNREACH', 'ENOTFOUND', 'EPROTO', 'ECONNABORTED'
];
return networkErrors.includes(error.code);
}
// By default, we can't determine if the error is retryable
return false;
}
/**
* Create a wrapped version of a function that catches errors
* and converts them to typed PlatformErrors
*
* @param fn Function to wrap
* @param errorCode Default error code to use
* @param contextData Additional context data
* @returns Wrapped function
*/
export function withErrorHandling<T extends (...args: any[]) => Promise<any>>(
fn: T,
errorCode: string,
contextData: Record<string, any> = {}
): T {
return (async function(...args: Parameters<T>): Promise<ReturnType<T>> {
try {
return await fn(...args);
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
// Already a typed error, rethrow
throw error;
}
throw fromError(
error instanceof Error ? error : new Error(String(error)),
errorCode,
{
...contextData,
fnName: fn.name,
args: args.map(arg =>
typeof arg === 'object'
? '[Object]'
: String(arg).substring(0, 100)
)
}
);
}
}) as T;
}
/**
* Retry a function with exponential backoff
*
* @param fn Function to retry
* @param options Retry options
* @returns Function result or throws after max retries
*/
export async function retry<T>(
fn: () => Promise<T>,
options: {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
backoffFactor?: number;
retryableErrors?: Array<string | RegExp>;
} = {}
): Promise<T> {
const {
maxRetries = 3,
initialDelay = 1000,
maxDelay = 30000,
backoffFactor = 2,
retryableErrors = []
} = options;
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error
? error
: new Error(String(error));
// Check if we should retry
const shouldRetry = attempt < maxRetries && (
isRetryable(error) ||
retryableErrors.some(pattern => {
if (typeof pattern === 'string') {
return lastError.message.includes(pattern);
}
return pattern.test(lastError.message);
})
);
if (!shouldRetry) {
throw lastError;
}
// Calculate delay with exponential backoff
const delay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
// Add jitter to prevent thundering herd problem (±20%)
const jitter = 0.8 + Math.random() * 0.4;
const actualDelay = Math.floor(delay * jitter);
// Wait before next retry
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
}
// This should never happen, but TypeScript needs it
throw lastError!;
}

611
ts/errors/mta.errors.ts Normal file
View File

@ -0,0 +1,611 @@
import {
PlatformError,
NetworkError,
AuthenticationError,
OperationError,
ConfigurationError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
MTA_CONNECTION_ERROR,
MTA_AUTHENTICATION_ERROR,
MTA_DELIVERY_ERROR,
MTA_CONFIGURATION_ERROR,
MTA_DNS_ERROR,
MTA_TIMEOUT_ERROR,
MTA_PROTOCOL_ERROR
} from './error.codes.js';
/**
* Base class for MTA connection errors
*/
export class MtaConnectionError extends NetworkError {
/**
* Creates a new MTA connection error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_CONNECTION_ERROR, context);
}
/**
* Creates an instance for a DNS resolution error
*
* @param hostname Hostname that failed to resolve
* @param originalError Original error
* @param context Additional context
*/
public static dnsError(
hostname: string,
originalError?: Error,
context: IErrorContext = {}
): MtaConnectionError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaConnectionError(
`Failed to resolve DNS for ${hostname}${errorMsg}`,
{
...context,
data: {
...context.data,
hostname,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not connect to mail server for ${hostname}.`
}
);
}
/**
* Creates an instance for a connection timeout
*
* @param hostname Hostname that timed out
* @param port Port number
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static timeout(
hostname: string,
port: number,
timeout: number,
context: IErrorContext = {}
): MtaConnectionError {
return new MtaConnectionError(
`Connection to ${hostname}:${port} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
hostname,
port,
timeout
},
userMessage: `Connection to mail server timed out.`
}
);
}
/**
* Creates an instance for a connection refused error
*
* @param hostname Hostname that refused connection
* @param port Port number
* @param context Additional context
*/
public static refused(
hostname: string,
port: number,
context: IErrorContext = {}
): MtaConnectionError {
return new MtaConnectionError(
`Connection to ${hostname}:${port} refused`,
{
...context,
data: {
...context.data,
hostname,
port
},
userMessage: `Connection to mail server was refused.`
}
);
}
}
/**
* Error class for MTA authentication errors
*/
export class MtaAuthenticationError extends AuthenticationError {
/**
* Creates a new MTA authentication error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_AUTHENTICATION_ERROR, context);
}
/**
* Creates an instance for invalid credentials
*
* @param hostname Hostname where authentication failed
* @param username Username that failed authentication
* @param context Additional context
*/
public static invalidCredentials(
hostname: string,
username: string,
context: IErrorContext = {}
): MtaAuthenticationError {
return new MtaAuthenticationError(
`Authentication failed for user ${username} at ${hostname}`,
{
...context,
data: {
...context.data,
hostname,
username
},
userMessage: `Authentication to mail server failed.`
}
);
}
/**
* Creates an instance for unsupported authentication method
*
* @param hostname Hostname
* @param method Authentication method that is not supported
* @param supportedMethods List of supported authentication methods
* @param context Additional context
*/
public static unsupportedMethod(
hostname: string,
method: string,
supportedMethods: string[] = [],
context: IErrorContext = {}
): MtaAuthenticationError {
return new MtaAuthenticationError(
`Authentication method ${method} not supported by ${hostname}${supportedMethods.length > 0 ? `. Supported methods: ${supportedMethods.join(', ')}` : ''}`,
{
...context,
data: {
...context.data,
hostname,
method,
supportedMethods
},
userMessage: `The mail server doesn't support the required authentication method.`
}
);
}
}
/**
* Error class for MTA delivery errors
*/
export class MtaDeliveryError extends OperationError {
/**
* Creates a new MTA delivery error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_DELIVERY_ERROR, context);
}
/**
* Creates an instance for a permanent delivery failure
*
* @param message Error message
* @param recipientAddress Recipient email address
* @param statusCode SMTP status code
* @param smtpResponse Full SMTP response
* @param context Additional context
*/
public static permanent(
message: string,
recipientAddress: string,
statusCode?: string,
smtpResponse?: string,
context: IErrorContext = {}
): MtaDeliveryError {
const statusCodeStr = statusCode ? ` (${statusCode})` : '';
return new MtaDeliveryError(
`Permanent delivery failure to ${recipientAddress}${statusCodeStr}: ${message}`,
{
...context,
data: {
...context.data,
recipientAddress,
statusCode,
smtpResponse,
permanent: true
},
userMessage: `The email could not be delivered to ${recipientAddress}.`
}
);
}
/**
* Creates an instance for a temporary delivery failure
*
* @param message Error message
* @param recipientAddress Recipient email address
* @param statusCode SMTP status code
* @param smtpResponse Full SMTP response
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
* @param context Additional context
*/
public static temporary(
message: string,
recipientAddress: string,
statusCode?: string,
smtpResponse?: string,
maxRetries: number = 3,
currentRetry: number = 0,
retryDelay: number = 60000,
context: IErrorContext = {}
): MtaDeliveryError {
const statusCodeStr = statusCode ? ` (${statusCode})` : '';
const error = new MtaDeliveryError(
`Temporary delivery failure to ${recipientAddress}${statusCodeStr}: ${message}`,
{
...context,
data: {
...context.data,
recipientAddress,
statusCode,
smtpResponse,
permanent: false
},
userMessage: `The email delivery to ${recipientAddress} failed temporarily. It will be retried.`
}
);
return error.withRetry(maxRetries, currentRetry, retryDelay) as MtaDeliveryError;
}
/**
* Check if this is a permanent delivery failure
*/
public isPermanent(): boolean {
return !!this.context.data?.permanent;
}
/**
* Get the recipient address associated with this delivery error
*/
public getRecipientAddress(): string | undefined {
return this.context.data?.recipientAddress;
}
/**
* Get the SMTP status code associated with this delivery error
*/
public getStatusCode(): string | undefined {
return this.context.data?.statusCode;
}
}
/**
* Error class for MTA configuration errors
*/
export class MtaConfigurationError extends ConfigurationError {
/**
* Creates a new MTA configuration error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_CONFIGURATION_ERROR, context);
}
/**
* Creates an instance for a missing configuration value
*
* @param propertyPath Path to the missing property
* @param context Additional context
*/
public static missingConfig(
propertyPath: string,
context: IErrorContext = {}
): MtaConfigurationError {
return new MtaConfigurationError(
`Missing required configuration: ${propertyPath}`,
{
...context,
data: {
...context.data,
propertyPath
},
userMessage: `The mail server is missing required configuration.`
}
);
}
/**
* Creates an instance for an invalid configuration value
*
* @param propertyPath Path to the invalid property
* @param value Current value
* @param expectedType Expected type or format
* @param context Additional context
*/
public static invalidConfig(
propertyPath: string,
value: any,
expectedType: string,
context: IErrorContext = {}
): MtaConfigurationError {
return new MtaConfigurationError(
`Invalid configuration value for ${propertyPath}: got ${value} (${typeof value}), expected ${expectedType}`,
{
...context,
data: {
...context.data,
propertyPath,
value,
expectedType
},
userMessage: `The mail server has an invalid configuration value.`
}
);
}
}
/**
* Error class for MTA DNS errors
*/
export class MtaDnsError extends NetworkError {
/**
* Creates a new MTA DNS error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_DNS_ERROR, context);
}
/**
* Creates an instance for an MX record lookup failure
*
* @param domain Domain that failed MX lookup
* @param originalError Original error
* @param context Additional context
*/
public static mxLookupFailed(
domain: string,
originalError?: Error,
context: IErrorContext = {}
): MtaDnsError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaDnsError(
`Failed to lookup MX records for ${domain}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
recordType: 'MX',
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not find mail servers for ${domain}.`
}
);
}
/**
* Creates an instance for a TXT record lookup failure
*
* @param domain Domain that failed TXT lookup
* @param recordPrefix Optional record prefix (e.g., 'spf', 'dkim', 'dmarc')
* @param originalError Original error
* @param context Additional context
*/
public static txtLookupFailed(
domain: string,
recordPrefix?: string,
originalError?: Error,
context: IErrorContext = {}
): MtaDnsError {
const recordType = recordPrefix ? `${recordPrefix} TXT` : 'TXT';
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaDnsError(
`Failed to lookup ${recordType} records for ${domain}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
recordType,
recordPrefix,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not verify ${recordPrefix || ''} records for ${domain}.`
}
);
}
}
/**
* Error class for MTA timeout errors
*/
export class MtaTimeoutError extends NetworkError {
/**
* Creates a new MTA timeout error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_TIMEOUT_ERROR, context);
}
/**
* Creates an instance for an SMTP command timeout
*
* @param command SMTP command that timed out
* @param server Server hostname
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static commandTimeout(
command: string,
server: string,
timeout: number,
context: IErrorContext = {}
): MtaTimeoutError {
return new MtaTimeoutError(
`SMTP command ${command} to ${server} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
command,
server,
timeout
},
userMessage: `The mail server took too long to respond.`
}
);
}
/**
* Creates an instance for an overall transaction timeout
*
* @param server Server hostname
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static transactionTimeout(
server: string,
timeout: number,
context: IErrorContext = {}
): MtaTimeoutError {
return new MtaTimeoutError(
`SMTP transaction with ${server} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
server,
timeout
},
userMessage: `The mail server transaction took too long to complete.`
}
);
}
}
/**
* Error class for MTA protocol errors
*/
export class MtaProtocolError extends OperationError {
/**
* Creates a new MTA protocol error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_PROTOCOL_ERROR, context);
}
/**
* Creates an instance for an unexpected server response
*
* @param command SMTP command that received unexpected response
* @param response Unexpected response
* @param expected Expected response pattern
* @param server Server hostname
* @param context Additional context
*/
public static unexpectedResponse(
command: string,
response: string,
expected: string,
server: string,
context: IErrorContext = {}
): MtaProtocolError {
return new MtaProtocolError(
`Unexpected SMTP response from ${server} for command ${command}: got "${response}", expected "${expected}"`,
{
...context,
data: {
...context.data,
command,
response,
expected,
server
},
userMessage: `Received an unexpected response from the mail server.`
}
);
}
/**
* Creates an instance for a syntax error
*
* @param details Error details
* @param server Server hostname
* @param context Additional context
*/
public static syntaxError(
details: string,
server: string,
context: IErrorContext = {}
): MtaProtocolError {
return new MtaProtocolError(
`SMTP syntax error in communication with ${server}: ${details}`,
{
...context,
data: {
...context.data,
details,
server
},
userMessage: `There was a protocol error communicating with the mail server.`
}
);
}
}

View File

@ -0,0 +1,352 @@
import {
PlatformError,
OperationError,
ResourceError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
REPUTATION_CHECK_ERROR,
REPUTATION_DATA_ERROR,
REPUTATION_BLOCKLIST_ERROR,
REPUTATION_UPDATE_ERROR,
WARMUP_ALLOCATION_ERROR,
WARMUP_LIMIT_EXCEEDED,
WARMUP_SCHEDULE_ERROR
} from './error.codes.js';
/**
* Base class for reputation-related errors
*/
export class ReputationError extends OperationError {
/**
* Creates a new reputation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(message, code, context);
}
}
/**
* Error class for reputation check errors
*/
export class ReputationCheckError extends ReputationError {
/**
* Creates a new reputation check error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_CHECK_ERROR, context);
}
/**
* Creates an instance for an IP reputation check error
*
* @param ip IP address
* @param provider Reputation provider
* @param originalError Original error
* @param context Additional context
*/
public static ipCheckFailed(
ip: string,
provider: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationCheckError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationCheckError(
`Failed to check reputation for IP ${ip} with provider ${provider}${errorMsg}`,
{
...context,
data: {
...context.data,
ip,
provider,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
/**
* Creates an instance for a domain reputation check error
*
* @param domain Domain
* @param provider Reputation provider
* @param originalError Original error
* @param context Additional context
*/
public static domainCheckFailed(
domain: string,
provider: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationCheckError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationCheckError(
`Failed to check reputation for domain ${domain} with provider ${provider}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
provider,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
}
/**
* Error class for reputation data errors
*/
export class ReputationDataError extends ReputationError {
/**
* Creates a new reputation data error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_DATA_ERROR, context);
}
/**
* Creates an instance for a data access error
*
* @param entity Entity type (domain, ip)
* @param entityId Entity identifier
* @param operation Operation that failed (read, write, update)
* @param originalError Original error
* @param context Additional context
*/
public static dataAccessFailed(
entity: string,
entityId: string,
operation: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationDataError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationDataError(
`Failed to ${operation} reputation data for ${entity} ${entityId}${errorMsg}`,
{
...context,
data: {
...context.data,
entity,
entityId,
operation,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
}
/**
* Error class for blocklist-related errors
*/
export class BlocklistError extends ReputationError {
/**
* Creates a new blocklist error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_BLOCKLIST_ERROR, context);
}
/**
* Creates an instance for an entity found on a blocklist
*
* @param entity Entity type (domain, ip)
* @param entityId Entity identifier
* @param blocklist Blocklist name
* @param reason Reason for listing (if available)
* @param context Additional context
*/
public static entityBlocked(
entity: string,
entityId: string,
blocklist: string,
reason?: string,
context: IErrorContext = {}
): BlocklistError {
const reasonText = reason ? ` (${reason})` : '';
return new BlocklistError(
`${entity.charAt(0).toUpperCase() + entity.slice(1)} ${entityId} is listed on blocklist ${blocklist}${reasonText}`,
{
...context,
data: {
...context.data,
entity,
entityId,
blocklist,
reason
},
userMessage: `The ${entity} ${entityId} is on a blocklist. This may affect email deliverability.`
}
);
}
}
/**
* Error class for reputation update errors
*/
export class ReputationUpdateError extends ReputationError {
/**
* Creates a new reputation update error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_UPDATE_ERROR, context);
}
}
/**
* Error class for IP warmup allocation errors
*/
export class WarmupAllocationError extends ReputationError {
/**
* Creates a new warmup allocation error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_ALLOCATION_ERROR, context);
}
/**
* Creates an instance for no available IPs
*
* @param domain Domain requesting an IP
* @param policy Allocation policy that was used
* @param context Additional context
*/
public static noAvailableIps(
domain: string,
policy: string,
context: IErrorContext = {}
): WarmupAllocationError {
return new WarmupAllocationError(
`No available IPs for domain ${domain} using ${policy} allocation policy`,
{
...context,
data: {
...context.data,
domain,
policy
},
userMessage: `No available sending IPs for ${domain}.`
}
);
}
}
/**
* Error class for IP warmup limit exceeded errors
*/
export class WarmupLimitError extends ResourceError {
/**
* Creates a new warmup limit error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_LIMIT_EXCEEDED, context);
}
/**
* Creates an instance for daily sending limit exceeded
*
* @param ip IP address
* @param domain Domain
* @param limit Daily limit
* @param sent Number of emails sent
* @param context Additional context
*/
public static dailyLimitExceeded(
ip: string,
domain: string,
limit: number,
sent: number,
context: IErrorContext = {}
): WarmupLimitError {
return new WarmupLimitError(
`Daily sending limit exceeded for IP ${ip} and domain ${domain}: ${sent}/${limit}`,
{
...context,
data: {
...context.data,
ip,
domain,
limit,
sent
},
userMessage: `Daily sending limit reached for ${domain}.`
}
);
}
}
/**
* Error class for IP warmup schedule errors
*/
export class WarmupScheduleError extends ReputationError {
/**
* Creates a new warmup schedule error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_SCHEDULE_ERROR, context);
}
}

View File

@ -1,9 +1,91 @@
import * as plugins from './plugins.js';
import { randomUUID } from 'node:crypto';
export const logger = new plugins.smartlog.Smartlog({
// Map NODE_ENV to valid TEnvironment
const nodeEnv = process.env.NODE_ENV || 'production';
const envMap: Record<string, 'local' | 'test' | 'staging' | 'production'> = {
'development': 'local',
'test': 'test',
'staging': 'staging',
'production': 'production'
};
// Default Smartlog instance
const baseLogger = new plugins.smartlog.Smartlog({
logContext: {
environment: 'production',
environment: envMap[nodeEnv] || 'production',
runtime: 'node',
zone: 'serve.zone',
}
});
// Extended logger compatible with the original enhanced logger API
class StandardLogger {
private defaultContext: Record<string, any> = {};
private correlationId: string | null = null;
constructor() {}
// Log methods
public log(level: 'error' | 'warn' | 'info' | 'success' | 'debug', message: string, context: Record<string, any> = {}) {
const combinedContext = {
...this.defaultContext,
...context
};
if (this.correlationId) {
combinedContext.correlation_id = this.correlationId;
}
baseLogger.log(level, message, combinedContext);
}
public error(message: string, context: Record<string, any> = {}) {
this.log('error', message, context);
}
public warn(message: string, context: Record<string, any> = {}) {
this.log('warn', message, context);
}
public info(message: string, context: Record<string, any> = {}) {
this.log('info', message, context);
}
public success(message: string, context: Record<string, any> = {}) {
this.log('success', message, context);
}
public debug(message: string, context: Record<string, any> = {}) {
this.log('debug', message, context);
}
// Context management
public setContext(context: Record<string, any>, overwrite: boolean = false) {
if (overwrite) {
this.defaultContext = context;
} else {
this.defaultContext = {
...this.defaultContext,
...context
};
}
}
// Correlation ID management
public setCorrelationId(id: string | null = null): string {
this.correlationId = id || randomUUID();
return this.correlationId;
}
public getCorrelationId(): string | null {
return this.correlationId;
}
public clearCorrelationId(): void {
this.correlationId = null;
}
}
// Export a singleton instance
export const logger = new StandardLogger();

View File

@ -10,19 +10,11 @@ import { logger } from '../../logger.js';
import type { SzPlatformService } from '../../platformservice.js';
// Import MTA service
import { MtaService, type IMtaConfig } from '../delivery/classes.mta.js';
import { MtaService } from '../delivery/classes.mta.js';
export interface IEmailConstructorOptions {
useMta?: boolean;
mtaConfig?: IMtaConfig;
templateConfig?: {
from?: string;
replyTo?: string;
footerHtml?: string;
footerText?: string;
};
loadTemplatesFromDir?: boolean;
}
// Import configuration interfaces
import type { IEmailConfig } from '../../config/email.config.js';
import { ConfigValidator, emailConfigSchema } from '../../config/index.js';
/**
* Options for sending an email
@ -149,19 +141,21 @@ export class EmailService {
public bounceManager: BounceManager;
// configuration
private config: IEmailConstructorOptions;
private config: IEmailConfig;
constructor(platformServiceRefArg: SzPlatformService, options: IEmailConstructorOptions = {}) {
constructor(platformServiceRefArg: SzPlatformService, options: IEmailConfig = {}) {
this.platformServiceRef = platformServiceRefArg;
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
// Set default options
this.config = {
useMta: options.useMta ?? true,
mtaConfig: options.mtaConfig || {},
templateConfig: options.templateConfig || {},
loadTemplatesFromDir: options.loadTemplatesFromDir ?? true
};
// Validate and apply defaults to configuration
const validationResult = ConfigValidator.validate(options, emailConfigSchema);
if (!validationResult.valid) {
logger.warn(`Email service configuration has validation errors: ${validationResult.errors.join(', ')}`);
}
// Set configuration with defaults
this.config = validationResult.config;
// Initialize validator
this.emailValidator = new EmailValidator();

View File

@ -25,6 +25,11 @@ export const logsDir = plugins.path.join(dataDir, 'logs'); // For logs
export const emailTemplatesDir = plugins.path.join(dataDir, 'templates', 'email');
export const MtaAttachmentsDir = plugins.path.join(dataDir, 'attachments'); // For email attachments
// Configuration path
export const configPath = process.env.CONFIG_PATH
? process.env.CONFIG_PATH
: plugins.path.join(baseDir, 'config.json');
// Create directories if they don't exist
export function ensureDirectories() {
// Ensure data directories

View File

@ -4,6 +4,9 @@ import { PlatformServiceDb } from './classes.platformservicedb.js'
import { EmailService } from './mail/services/classes.emailservice.js';
import { SmsService } from './sms/classes.smsservice.js';
import { MtaService } from './mail/delivery/classes.mta.js';
import { logger } from './logger.js';
import { type IPlatformConfig } from './config/index.js';
import { ConfigurationError } from './errors/base.errors.js';
export class SzPlatformService {
public projectinfo: plugins.projectinfo.ProjectInfo;
@ -17,29 +20,169 @@ export class SzPlatformService {
public emailService: EmailService;
public mtaService: MtaService;
public smsService: SmsService;
// Platform configuration
public config: IPlatformConfig;
public async start() {
this.platformserviceDb = new PlatformServiceDb(this);
/**
* Create a new platform service instance
*
* @param config Optional platform configuration
*/
constructor(config: IPlatformConfig) {
// Store configuration
this.config = config;
// Initialize typed router
this.typedrouter = new plugins.typedrequest.TypedRouter();
}
/**
* Initialize the platform service
* Applies configuration provided in constructor
*/
public async initialize(): Promise<void> {
// Simple validation of config - must be provided
if (!this.config) {
throw new ConfigurationError(
'Platform configuration must be provided in constructor',
'PLATFORM_CONFIG_MISSING',
{}
);
}
// Apply configuration to logger
if (this.config.logging) {
logger.setContext({
environment: this.config.environment,
component: 'PlatformService'
});
}
// Create project info
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// lets start the sub services
this.emailService = new EmailService(this);
this.mtaService = new MtaService(this);
this.smsService = new SmsService(this, {
apiGatewayApiToken: await this.serviceQenv.getEnvVarOnDemand('SMS_API_TOKEN'),
});
// Initialize database
this.platformserviceDb = new PlatformServiceDb(this);
// lets start the server finally
this.typedserver = new plugins.typedserver.TypedServer({
cors: true,
});
await this.typedserver.start();
logger.info('Platform service initialized successfully');
}
/**
* Start the platform service
*/
public async start(): Promise<void> {
// Initialize first if needed
if (!this.config) {
await this.initialize();
}
// Check if service is enabled
if (this.config.enabled === false) {
logger.warn('Platform service is disabled in configuration, not starting services');
return;
}
logger.info('Starting platform service...');
// Initialize sub-services
await this.initializeServices();
// Start the HTTP server
await this.startServer();
logger.info('Platform service started successfully');
}
public async stop() {
/**
* Initialize and start sub-services
*/
private async initializeServices(): Promise<void> {
// Initialize email service
if (this.config.email?.enabled !== false) {
this.emailService = new EmailService(this, this.config.email);
await this.emailService.start();
logger.info('Email service started');
// Initialize MTA service if needed
if (this.config.email?.useMta) {
this.mtaService = new MtaService(this, this.config.email.mtaConfig);
logger.info('MTA service initialized');
}
} else {
logger.info('Email service disabled in configuration');
}
// Initialize SMS service
if (this.config.sms?.enabled !== false) {
// Get API token from config or env var
const apiToken = this.config.sms?.apiGatewayApiToken ||
await this.serviceQenv.getEnvVarOnDemand('SMS_API_TOKEN');
if (!apiToken) {
logger.warn('No SMS API token provided, SMS service will not be started');
} else {
this.smsService = new SmsService(this, {
apiGatewayApiToken: apiToken,
...this.config.sms
});
await this.smsService.start();
logger.info('SMS service started');
}
} else {
logger.info('SMS service disabled in configuration');
}
}
/**
* Start the HTTP server
*/
private async startServer(): Promise<void> {
// Check if server is enabled
if (this.config.server?.enabled === false) {
logger.info('HTTP server disabled in configuration');
return;
}
// Create server with configuration
this.typedserver = new plugins.typedserver.TypedServer({
cors: this.config.server?.cors === false ? false : true,
port: this.config.server?.port || 3000,
// hostname is not supported directly, will be set during start
});
// Add the router
// Note: Using any type to bypass TypeScript restriction
(this.typedserver as any).addRouter(this.typedrouter);
// Start server
await this.typedserver.start();
logger.info(`HTTP server started on ${this.config.server?.host || '0.0.0.0'}:${this.config.server?.port || 3000}`);
}
/**
* Stop the platform service
*/
public async stop(): Promise<void> {
logger.info('Stopping platform service...');
// Stop sub-services
if (this.emailService) {
await this.emailService.stop();
logger.info('Email service stopped');
}
if (this.smsService) {
await this.smsService.stop();
logger.info('SMS service stopped');
}
// Stop the server if it's running
if (this.typedserver) {
await this.typedserver.stop();
logger.info('HTTP server stopped');
}
logger.info('Platform service stopped successfully');
}
}

View File

@ -55,6 +55,9 @@ import * as smartrx from '@push.rocks/smartrx';
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartlog, smartmail, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx };
// Define SmartLog types for use in error handling
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
// apiclient.xyz scope
import * as cloudflare from '@apiclient.xyz/cloudflare';

View File

@ -3,19 +3,29 @@ import * as paths from '../paths.js';
import { logger } from '../logger.js';
import type { SzPlatformService } from '../platformservice.js';
export interface ISmsConstructorOptions {
apiGatewayApiToken: string;
}
import type { ISmsConfig } from '../config/sms.config.js';
import { ConfigValidator, smsConfigSchema } from '../config/index.js';
export class SmsService {
public platformServiceRef: SzPlatformService;
public projectinfo: plugins.projectinfo.ProjectInfo;
public typedrouter = new plugins.typedrequest.TypedRouter();
public options: ISmsConstructorOptions;
public config: ISmsConfig;
constructor(platformServiceRefArg: SzPlatformService, optionsArg: ISmsConstructorOptions) {
constructor(platformServiceRefArg: SzPlatformService, options: ISmsConfig) {
this.platformServiceRef = platformServiceRefArg;
this.options = optionsArg;
// Validate and apply defaults to configuration
const validationResult = ConfigValidator.validate(options, smsConfigSchema);
if (!validationResult.valid) {
logger.warn(`SMS service configuration has validation errors: ${validationResult.errors.join(', ')}`);
}
// Set configuration with defaults
this.config = validationResult.config;
// Add router to platform service
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
}
@ -53,8 +63,11 @@ export class SmsService {
}
public async sendSms(toNumber: number, fromName: string, messageText: string) {
// Use default sender if not specified
const sender = fromName || this.config.defaultSender || 'PlatformService';
const payload = {
sender: fromName,
sender,
message: messageText,
recipients: [{ msisdn: toNumber }],
};
@ -63,7 +76,7 @@ export class SmsService {
method: 'POST',
requestBody: JSON.stringify(payload),
headers: {
Authorization: `Basic ${Buffer.from(`${this.options.apiGatewayApiToken}:`).toString('base64')}`,
Authorization: `Basic ${Buffer.from(`${this.config.apiGatewayApiToken}:`).toString('base64')}`,
'Content-Type': 'application/json',
},
});