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

View File

@ -1,5 +1,27 @@
# Changelog
## 2025-05-08 - 2.10.0 - feat(config): Implement standardized configuration system
Create a comprehensive configuration system with validation, defaults, and documentation
- Added consistent configuration interfaces across all services
- Implemented validation for all configuration objects with detailed error reporting
- Added default values for optional configuration parameters
- Created an extensive documentation system for configuration options
- Added migration helpers for managing configuration format changes
- Enhanced platform service to load configuration from multiple sources (file, environment, code)
- Updated email and SMS services to use the new configuration system
## 2025-05-08 - 2.9.0 - feat(errors): Implement comprehensive error handling system
Enhance error handling with structured errors, consistent patterns, and improved logging
- Added domain-specific error classes for better error categorization and handling
- Created comprehensive error codes for all service types (email, MTA, security, etc.)
- Implemented detailed error context with severity, category, and recoverability classification
- Added utilities for error conversion, formatting, and handling with automatic retry mechanisms
- Enhanced logging with correlation tracking, context support, and structured data
- Created middleware for handling errors in HTTP requests with proper status code mapping
- Added retry with exponential backoff for transient failures
## 2025-05-08 - 2.8.9 - fix(types)
Fix TypeScript build errors and improve API type safety across platformservice interfaces
@ -157,86 +179,7 @@ Enhance email integration by updating @push.rocks/smartmail to ^2.1.0 and improv
- Updated readme.plan.md with detailed roadmap for further performance, security, analytics, and deliverability enhancements
- Expanded test suite to cover smartmail integration, validation, templating, and conversion between formats
## 2025-05-04 - 2.3.1 - fix(platformservice)
Update dependency versions and refactor import paths for improved compatibility; add initial DcRouter plan documentation.
- Upgrade @git.zone/tsbuild to ^2.3.2 and @push.rocks/tapbundle to ^6.0.3.
- Upgrade @api.global/typedserver to ^3.0.74 and update related API dependencies (cloudflare, letterxpress).
- Upgrade smartdata to ^5.15.1, add smartdns (^6.2.2), upgrade smartproxy to ^10.0.2, smartrequest to ^2.1.0, smartrule to ^2.0.1, and smartrx to ^3.0.10.
- Upgrade @serve.zone/interfaces to ^5.0.4 and @tsclass/tsclass to ^9.1.0; update mailauth to ^4.8.4.
- Add packageManager field in package.json for PNPM configuration.
- Add readme.plan.md detailing the DcRouter implementation plan.
- Refactor import paths in several TS files (e.g. ts/plugins.ts, ts/mta classes) for consistency.
## 2025-03-15 - 2.3.0 - feat(platformservice)
Add AIBridge module and refactor service file paths for improved module organization
- Added new AIBridge class in ts/aibridge/classes.aibridge.ts.
- Renamed letter service file from ts/letter/letterservice.ts to ts/letter/classes.letterservice.ts and updated its index.
- Updated platformservice.ts to import letter and SMS services from new paths.
- Renamed SMS service file from ts/sms/smsservice.ts to ts/sms/classes.smsservice.ts and updated its index accordingly.
## 2025-03-15 - 2.2.1 - fix(platformservice)
Refactor module structure to update import paths and file organization
- Removed obsolete file 'ts/classes.platformservice.ts' and updated references to use 'ts/platformservice.ts'.
- Updated import paths in PlatformServiceDb, EmailService, and other modules to use new file structure.
- Renamed and moved files in the email, mta, letter, and sms directories to align with new module layout.
- Fixed references to external modules (e.g. '@serve.zone/interfaces', '@push.rocks/*', etc.) to reflect the updated paths.
## 2025-03-15 - 2.2.0 - feat(plugins)
Add smartproxy support by including the @push.rocks/smartproxy dependency and exporting it in the plugins module.
- Added '@push.rocks/smartproxy' dependency version '^4.1.0' to package.json
- Updated ts/plugins.ts to export the smartproxy module alongside other push.rocks modules
## 2025-03-15 - 2.1.0 - feat(MTA)
Update readme with detailed Mail Transfer Agent usage and examples
- Added a comprehensive MTA section with usage examples including SMTP server setup, DKIM signing/verification, SPF/DMARC support, and API integration
- Expanded the conclusion to highlight MTA capabilities alongside email, SMS, letter, and AI services
## 2025-03-15 - 2.0.0 - BREAKING CHANGE(platformservice)
Remove deprecated AIBridge module and update email service to use the MTA connector; update dependency versions and adjust build scripts in package.json.
- Completely remove the aibridge module files (aibridge.classes.aibridge.ts, aibridge.classes.aibridgedb.ts, aibridge.classes.openaibridge.ts, aibridge.paths.ts, aibridge.plugins.ts, and index.ts) as they are no longer needed.
- Switch the email service from using MailgunConnector to the new MTA connector for sending emails.
- Update dependency versions for @serve.zone/interfaces, @tsclass/tsclass, letterxpress, and uuid in package.json.
- Enhance the build script in package.json and add pnpm configuration.
## 2025-03-15 - 1.1.2 - fix(mta)
Expose HttpResponse.statusCode and add explicit generic type annotations in DNSManager cache retrieval
- Changed HttpResponse.statusCode from private to public to allow external access and inspection
- Added explicit generic type parameters in getFromCache calls for lookupMx and lookupTxt to enhance type safety
## 2025-03-15 - 1.1.1 - fix(paths)
Update directory paths to use a dedicated 'data' directory and add ensureDirectories function for proper directory creation.
- Refactored ts/paths.ts to define a base data directory using process.cwd().
- Reorganized MTA directories (keys, dns, emails sent/received/failed, logs) under the data directory.
- Added ensureDirectories function to create missing directories at runtime.
## 2025-03-15 - 1.1.1 - fix(mta)
Refactor API Manager and DKIMCreator: remove Express dependency in favor of Node's native HTTP server, add an HttpResponse helper to improve request handling, update path and authentication logic, and expose previously private DKIMCreator methods for API access.
- Replaced Express-based middleware with native HTTP server handling, including request body parsing and CORS headers.
- Introduced an HttpResponse helper class to standardize response writing.
- Updated route matching, parameter extraction, and error handling within the API Manager.
- Modified DKIMCreator methods (createDKIMKeys, storeDKIMKeys, createAndStoreDKIMKeys, and getDNSRecordForDomain) from private to public for better API accessibility.
- Updated plugin imports to include the native HTTP module.
## 2025-03-15 - 1.1.0 - feat(mta)
Enhance MTA service and SMTP server with robust session management, advanced email handling, and integrated API routes
- Introduce a state machine (SmtpState) and session management in the SMTP server to replace legacy buffering
- Refactor DNSManager with caching and improved SPF, DKIM, and DMARC verification methods
- Update Email class to support multiple recipients, CC, BCC with input sanitization and validation
- Add detailed logging, TLS upgrade handling, and error-based retry logic in EmailSendJob
- Implement a new API Manager with typed routes for sending emails, DKIM key generation, domain verification, and statistics
- Integrate certificate provisioning with auto-renewal and TLS options in the MTA service configuration
## 2024-05-11 - 1.0.10 to 1.0.8 - core
## 2025-05-04 - 1.0.10 to 1.0.8 - core
Applied core fixes across several versions on this day.
- Fixed core issues in versions 1.0.10, 1.0.9, and 1.0.8
@ -262,4 +205,4 @@ Applied a core fix.
- Fixed core functionality for version 1.0.1
Note: Versions that only contained version bumps (for example, 1.0.11 and the plain “1.0.x” commits) have been omitted from individual entries and are implicitly included in the version ranges above.
Note: Versions that only contained version bumps (for example, 1.0.11 and the plain "1.0.x" commits) have been omitted from individual entries and are implicitly included in the version ranges above.

View File

@ -1,7 +1,7 @@
{
"name": "@serve.zone/platformservice",
"private": true,
"version": "2.8.9",
"version": "2.10.0",
"description": "A multifaceted platform service handling mail, SMS, letter delivery, and AI services.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",

View File

@ -20,18 +20,18 @@
- [x] Set consistent test environment with NODE_ENV=test
### Error Handling Improvements
- [ ] Add structured error types for better error reporting
- [ ] Implement consistent error handling patterns across services
- [ ] Add detailed error context for debugging
- [ ] Create retry mechanisms for transient failures
- [ ] Improve error logging with structured data
- [x] Add structured error types for better error reporting
- [x] Implement consistent error handling patterns across services
- [x] Add detailed error context for debugging
- [x] Create retry mechanisms for transient failures
- [x] Improve error logging with structured data
### Configuration Interface Standardization
- [ ] Define consistent configuration interfaces across services
- [ ] Implement validation for all configuration objects
- [ ] Add default values for optional configuration
- [ ] Create documentation for all configuration options
- [ ] Add migration helpers for configuration format changes
- [x] Define consistent configuration interfaces across services
- [x] Implement validation for all configuration objects
- [x] Add default values for optional configuration
- [x] Create documentation for all configuration options
- [x] Add migration helpers for configuration format changes
### Logging Enhancements
- [ ] Implement structured logging throughout the codebase

107
readme.smartlog.md Normal file
View File

@ -0,0 +1,107 @@
# Smartlog Improvement Plan
## Overview
This document outlines a plan for enhancing the `@push.rocks/smartlog` module to incorporate the advanced features currently implemented in the custom `EnhancedLogger` wrapper. By moving these features directly into `smartlog`, we can eliminate the need for wrapper classes while providing a more comprehensive logging solution.
## Current Limitations in Smartlog
- Limited context management (no hierarchical contexts)
- No correlation ID tracking for distributed tracing
- No built-in filtering or log level management
- No log sampling capabilities
- No middleware for HTTP request/response logging
- No timing utilities for performance tracking
- No child logger functionality with context inheritance
## Proposed Enhancements
### 1. Context Management
- Add hierarchical context support
- Implement methods for manipulating context:
- `setContext(context, overwrite = false)`
- `addToContext(key, value)`
- `removeFromContext(key)`
### 2. Correlation ID Tracking
- Add correlation ID support for distributed tracing
- Implement methods for correlation management:
- `setCorrelationId(id = null)`
- `getCorrelationId()`
- `clearCorrelationId()`
### 3. Log Filtering
- Implement configurable log filtering based on:
- Minimum log level
- Pattern-based exclusion rules
- Custom filtering functions
### 4. Log Sampling
- Add probabilistic log sampling for high-volume environments
- Support for enforcing critical logs (e.g., errors) regardless of sampling
### 5. Child Loggers
- Support creating child loggers with inherited context
- Allow context overrides in child loggers
### 6. Timing Utilities
- Add methods for timing operations:
- `logTimed(level, message, fn, context)`
- Support for both async and sync operations
### 7. HTTP Request Logging
- Add middleware for Express/Fastify/other HTTP frameworks
- Auto-capture request/response data
- Auto-propagate correlation IDs
### 8. Log Standardization
- Ensure consistent output format
- Add standard fields like timestamp, correlation ID
- Support for custom formatters
## Implementation Plan
1. **Core Enhancements**
- Implement context management
- Add correlation ID tracking
- Develop filtering and sampling capabilities
2. **Extended Features**
- Build child logger functionality
- Create timing utility methods
- Implement HTTP middleware
3. **Compatibility**
- Ensure backward compatibility
- Provide migration guide
- Add TypeScript declarations
4. **Documentation**
- Update README with new features
- Add examples for each feature
- Document best practices
## Migration Path
After implementing these enhancements to `smartlog`, the migration would involve:
1. Update to latest `smartlog` version
2. Replace `EnhancedLogger` instances with `smartlog.Smartlog`
3. Update configuration to use new capabilities
4. Replace middleware with `smartlog`'s built-in solutions
## Benefits
- Simplified dependency tree
- Better maintainability with single logging solution
- Improved performance with native implementation
- Enhanced type safety through TypeScript
- Standardized logging across projects

View File

@ -12,7 +12,42 @@ import { Email } from '../ts/mail/core/classes.email.js';
let platformService: SzPlatformService;
tap.test('Setup test environment', async () => {
platformService = new SzPlatformService();
// Create platform service with default config from the config module
platformService = new SzPlatformService({
id: 'test-platform-service',
version: '1.0.0',
environment: 'test',
name: 'TestPlatformService',
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.test.local',
maxSize: 10 * 1024 * 1024
},
security: {
useDkim: true,
verifyDkim: true,
verifySpf: true,
verifyDmarc: true
}
}
}
});
// Use start() instead of init() which doesn't exist
await platformService.start();
expect(platformService.mtaService).toBeTruthy();

367
test/test.errors.ts Normal file
View File

@ -0,0 +1,367 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as errors from '../ts/errors/index.js';
import {
PlatformError,
ValidationError,
NetworkError,
ResourceError,
OperationError
} from '../ts/errors/base.errors.js';
import {
ErrorSeverity,
ErrorCategory,
ErrorRecoverability
} from '../ts/errors/error.codes.js';
import {
EmailServiceError,
EmailTemplateError,
EmailValidationError,
EmailSendError,
EmailReceiveError
} from '../ts/errors/email.errors.js';
import {
MtaConnectionError,
MtaAuthenticationError,
MtaDeliveryError,
MtaConfigurationError
} from '../ts/errors/mta.errors.js';
import {
ErrorHandler
} from '../ts/errors/error-handler.js';
// Test base error classes
tap.test('Base error classes should set properties correctly', async () => {
const message = 'Test error message';
const code = 'TEST_ERROR_CODE';
const context = {
component: 'TestComponent',
operation: 'testOperation',
data: { foo: 'bar' }
};
// Test PlatformError
const platformError = new PlatformError(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
expect(platformError.message).toEqual(message);
expect(platformError.code).toEqual(code);
expect(platformError.severity).toEqual(ErrorSeverity.MEDIUM);
expect(platformError.category).toEqual(ErrorCategory.OPERATION);
expect(platformError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE);
expect(platformError.context.component).toEqual(context.component);
expect(platformError.context.operation).toEqual(context.operation);
expect(platformError.context.data.foo).toEqual('bar');
expect(platformError.name).toEqual('PlatformError');
// Test ValidationError
const validationError = new ValidationError(message, code, context);
expect(validationError.category).toEqual(ErrorCategory.VALIDATION);
expect(validationError.severity).toEqual(ErrorSeverity.LOW);
// Test NetworkError
const networkError = new NetworkError(message, code, context);
expect(networkError.category).toEqual(ErrorCategory.CONNECTIVITY);
expect(networkError.severity).toEqual(ErrorSeverity.MEDIUM);
expect(networkError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE);
// Test ResourceError
const resourceError = new ResourceError(message, code, context);
expect(resourceError.category).toEqual(ErrorCategory.RESOURCE);
});
// Test email error classes
tap.test('Email error classes should be properly constructed', async () => {
// Test EmailServiceError
const emailServiceError = new EmailServiceError('Email service error', {
component: 'EmailService',
operation: 'sendEmail'
});
expect(emailServiceError.code).toEqual('EMAIL_SERVICE_ERROR');
expect(emailServiceError.name).toEqual('EmailServiceError');
// Test EmailTemplateError
const templateError = new EmailTemplateError('Template not found: welcome_email', {
data: { templateId: 'welcome_email' }
});
expect(templateError.code).toEqual('EMAIL_TEMPLATE_ERROR');
expect(templateError.context.data.templateId).toEqual('welcome_email');
// Test EmailSendError with permanent flag
const permanentError = EmailSendError.permanent(
'Invalid recipient',
'user@example.com',
{ data: { details: 'DNS not found' } }
);
expect(permanentError.code).toEqual('EMAIL_SEND_ERROR');
expect(permanentError.isPermanent()).toEqual(true);
expect(permanentError.context.data.permanent).toEqual(true);
// Test EmailSendError with temporary flag and retry
const tempError = EmailSendError.temporary(
'Server busy',
3,
0,
1000,
{ data: { server: 'smtp.example.com' } }
);
expect(tempError.isPermanent()).toEqual(false);
expect(tempError.context.data.permanent).toEqual(false);
expect(tempError.context.retry.maxRetries).toEqual(3);
expect(tempError.shouldRetry()).toEqual(true);
});
// Test MTA error classes
tap.test('MTA error classes should be properly constructed', async () => {
// Test MtaConnectionError
const dnsError = MtaConnectionError.dnsError('mail.example.com', new Error('DNS lookup failed'));
expect(dnsError.code).toEqual('MTA_CONNECTION_ERROR');
expect(dnsError.category).toEqual(ErrorCategory.CONNECTIVITY);
expect(dnsError.context.data.hostname).toEqual('mail.example.com');
// Test MtaTimeoutError via MtaConnectionError.timeout
const timeoutError = MtaConnectionError.timeout('mail.example.com', 25, 30000);
expect(timeoutError.code).toEqual('MTA_CONNECTION_ERROR');
expect(timeoutError.context.data.timeout).toEqual(30000);
// Test MtaAuthenticationError
const authError = MtaAuthenticationError.invalidCredentials('mail.example.com', 'user@example.com');
expect(authError.code).toEqual('MTA_AUTHENTICATION_ERROR');
expect(authError.category).toEqual(ErrorCategory.AUTHENTICATION);
expect(authError.context.data.username).toEqual('user@example.com');
// Test MtaDeliveryError
const permDeliveryError = MtaDeliveryError.permanent(
'User unknown',
'nonexistent@example.com',
'550',
'550 5.1.1 User unknown'
);
expect(permDeliveryError.code).toEqual('MTA_DELIVERY_ERROR');
expect(permDeliveryError.isPermanent()).toEqual(true);
expect(permDeliveryError.getRecipientAddress()).toEqual('nonexistent@example.com');
expect(permDeliveryError.getStatusCode()).toEqual('550');
// Test temporary delivery error with retry
const tempDeliveryError = MtaDeliveryError.temporary(
'Mailbox temporarily unavailable',
'user@example.com',
'450',
'450 4.2.1 Mailbox temporarily unavailable',
3,
1,
5000
);
expect(tempDeliveryError.isPermanent()).toEqual(false);
expect(tempDeliveryError.shouldRetry()).toEqual(true);
expect(tempDeliveryError.context.retry.currentRetry).toEqual(1);
expect(tempDeliveryError.context.retry.maxRetries).toEqual(3);
});
// Test error handler utility
tap.test('ErrorHandler should properly handle and format errors', async () => {
// Configure error handler
ErrorHandler.configure({
logErrors: false, // Disable for testing
includeStacksInProd: false,
retry: {
maxAttempts: 5,
baseDelay: 100,
maxDelay: 1000,
backoffFactor: 2
}
});
// Test converting regular Error to PlatformError
const regularError = new Error('Something went wrong');
const platformError = ErrorHandler.toPlatformError(
regularError,
'PLATFORM_OPERATION_ERROR',
{ component: 'TestHandler' }
);
expect(platformError).toBeInstanceOf(PlatformError);
expect(platformError.code).toEqual('PLATFORM_OPERATION_ERROR');
expect(platformError.context.component).toEqual('TestHandler');
// Test formatting error for API response
const formattedError = ErrorHandler.formatErrorForResponse(platformError, true);
expect(formattedError.code).toEqual('PLATFORM_OPERATION_ERROR');
expect(formattedError.message).toEqual('An unexpected error occurred.');
expect(formattedError.details.rawMessage).toEqual('Something went wrong');
// Test executing a function with error handling
let executed = false;
try {
await ErrorHandler.execute(async () => {
executed = true;
throw new Error('Execution failed');
}, 'TEST_EXECUTION_ERROR', { operation: 'testExecution' });
} catch (error) {
expect(error).toBeInstanceOf(PlatformError);
expect(error.code).toEqual('TEST_EXECUTION_ERROR');
expect(error.context.operation).toEqual('testExecution');
}
expect(executed).toEqual(true);
// Test executeWithRetry successful after retries
let attempts = 0;
const result = await ErrorHandler.executeWithRetry(
async () => {
attempts++;
if (attempts < 3) {
throw new Error('Temporary failure');
}
return 'success';
},
'TEST_RETRY_ERROR',
{
maxAttempts: 5,
baseDelay: 10, // Use small delay for tests
onRetry: (error, attempt, delay) => {
expect(error).toBeInstanceOf(PlatformError);
expect(attempt).toBeGreaterThan(0);
expect(delay).toBeGreaterThan(0);
}
}
);
expect(result).toEqual('success');
expect(attempts).toEqual(3);
// Test executeWithRetry that fails after max attempts
attempts = 0;
try {
await ErrorHandler.executeWithRetry(
async () => {
attempts++;
throw new Error('Persistent failure');
},
'TEST_RETRY_ERROR',
{
maxAttempts: 3,
baseDelay: 10
}
);
} catch (error) {
expect(error).toBeInstanceOf(PlatformError);
expect(attempts).toEqual(3);
}
});
// Test retry utilities
tap.test('Error retry utilities should work correctly', async () => {
let attempts = 0;
const start = Date.now();
try {
await errors.retry(
async () => {
attempts++;
if (attempts < 3) {
throw new Error('Temporary error');
}
return 'success';
},
{
maxRetries: 5,
initialDelay: 20,
backoffFactor: 1.5,
retryableErrors: [/Temporary/]
}
);
} catch (e) {
// Should not reach here
expect(false).toEqual(true);
}
expect(attempts).toEqual(3);
// Test retry with non-retryable error
attempts = 0;
try {
await errors.retry(
async () => {
attempts++;
throw new Error('Critical error');
},
{
maxRetries: 3,
initialDelay: 10,
retryableErrors: [/Temporary/] // Won't match "Critical"
}
);
} catch (error) {
expect(error.message).toEqual('Critical error');
expect(attempts).toEqual(1); // Should only attempt once
}
});
// Helper function that will reject first n times, then resolve
async function flaky(failTimes: number, result: any = 'success'): Promise<any> {
if (flaky.counter < failTimes) {
flaky.counter++;
throw new Error(`Flaky failure ${flaky.counter}`);
}
return result;
}
flaky.counter = 0;
flaky.reset = () => { flaky.counter = 0; };
// Test error wrapping and retry combination
tap.test('Error handling can be combined with retry for robust operations', async () => {
// Reset counter for the test
flaky.reset();
// Create a wrapped version of the flaky function
const wrapped = errors.withErrorHandling(
() => flaky(2, 'wrapped success'),
'TEST_WRAPPED_ERROR',
{ component: 'TestComponent' }
);
// Execute with retry
try {
const result = await errors.retry(
wrapped,
{
maxRetries: 3,
initialDelay: 10,
}
);
expect(result).toEqual('wrapped success');
expect(flaky.counter).toEqual(2);
} catch (error) {
// Should not reach here
expect(false).toEqual(true);
}
// Reset and test failure case
flaky.reset();
try {
await errors.retry(
() => flaky(5, 'never reached'),
{
maxRetries: 2, // Only retry twice, but we need 5 attempts to succeed
initialDelay: 10,
}
);
// Should not reach here
expect(false).toEqual(true);
} catch (error) {
expect(error.message).toContain('Flaky failure');
expect(flaky.counter).toEqual(3); // Initial + 2 retries = 3 attempts
}
});
tap.test('stop', async () => {
// This is a placeholder test to ensure we call tap.stopForcefully()
});
export default tap.stopForcefully();

View File

@ -25,8 +25,22 @@ tap.test('should be able to create an independent MTA service', async (tools) =>
});
tap.test('should be able to create an EmailService with an existing MTA', async (tools) => {
// Create a platform service first
const platformService = new SzPlatformService();
// Create a platform service with test config
const platformService = new SzPlatformService({
id: 'test-platform-service',
version: '1.0.0',
environment: 'test',
name: 'TestPlatformService',
enabled: true,
logging: {
level: 'info',
structured: true,
correlationTracking: true
},
server: {
enabled: false // Disable server for tests
}
});
// Create a shared bounce manager
const bounceManager = new BounceManager();
@ -74,8 +88,22 @@ tap.test('MTA service should have SMTP rule engine', async (tools) => {
});
tap.test('platform service should support having an MTA service', async (tools) => {
// Create a platform service with default config
const platformService = new SzPlatformService();
// Create a platform service with test config
const platformService = new SzPlatformService({
id: 'test-platform-service',
version: '1.0.0',
environment: 'test',
name: 'TestPlatformService',
enabled: true,
logging: {
level: 'info',
structured: true,
correlationTracking: true
},
server: {
enabled: false // Disable server for tests
}
});
// Create MTA - don't await start() to avoid binding to ports
platformService.mtaService = new MtaService(platformService, {

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',
},
});