Compare commits
No commits in common. "master" and "v2.8.6" have entirely different histories.
146
changelog.md
146
changelog.md
@ -1,68 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-08 - 2.11.1 - fix(platform)
|
||||
Update commit info with no functional changes; regenerated commit information.
|
||||
|
||||
|
||||
## 2025-05-08 - 2.11.0 - feat(platformservice)
|
||||
Expose DcRouter and update package visibility. Changed package.json 'private' flag from true to false to allow public publication, and added export of DcRouter in ts/index.ts for improved API accessibility.
|
||||
|
||||
- Changed package.json: set 'private' to false
|
||||
- Added export for DcRouter in ts/index.ts
|
||||
|
||||
## 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
|
||||
|
||||
- Fixed interface placement in EmailService and MtaConnector classes
|
||||
- Aligned DeliveryStatus enum and updated ApiManager handlers with proper type-safe signatures
|
||||
- Added comprehensive TypeScript interfaces for ISendEmailOptions, ITemplateContext, IValidateEmailOptions, IValidationResult, and IEmailServiceStats
|
||||
- Removed circular dependencies in type definitions and added proper type assertions
|
||||
- Improved test stability by handling race conditions in SenderReputationMonitor and IPWarmupManager; external DNS lookups are disabled under test environment
|
||||
|
||||
## 2025-05-08 - 2.8.8 - fix(types): Fix TypeScript build errors and improve API interfaces
|
||||
Fix TypeScript build errors caused by interface placement and improve API type alignment
|
||||
|
||||
- Fixed interface placement in EmailService and MtaConnector classes
|
||||
- Aligned DeliveryStatus enum with EmailSendJob implementation
|
||||
- Added proper method signatures for API endpoint handlers in ApiManager class
|
||||
- Updated getStats and checkEmailStatus methods to conform to API contracts
|
||||
- Implemented type-safe return values for all API methods
|
||||
- Fixed circular dependencies in type definitions
|
||||
- Added proper type assertion where needed to satisfy TypeScript compiler
|
||||
|
||||
## 2025-05-08 - 2.8.7 - feat(types): Add comprehensive TypeScript interfaces for API types
|
||||
Improve type safety across the platform by adding detailed TypeScript interfaces for APIs
|
||||
|
||||
- Added ISendEmailOptions interface with complete documentation for email sending options
|
||||
- Created ITemplateContext interface for email template rendering with full type safety
|
||||
- Added IValidateEmailOptions and IValidationResult interfaces for email validation
|
||||
- Improved IEmailServiceStats interface with detailed statistics types
|
||||
- Added IEmailStatusResponse and IEmailStatusDetails interfaces for MTA status checking
|
||||
- Updated sendEmail and other methods to use these new interfaces instead of 'any'
|
||||
- Removed need for type assertions in various components
|
||||
|
||||
## 2025-05-08 - 2.8.6 - fix(tests)
|
||||
fix: Improve test stability by handling race conditions in SenderReputationMonitor and IPWarmupManager. Disable filesystem operations and external DNS lookups during tests by checking NODE_ENV, add proper cleanup of singleton instances and active timeouts to ensure consistent test environment.
|
||||
|
||||
@ -189,7 +126,86 @@ 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 - 1.0.10 to 1.0.8 - core
|
||||
## 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
|
||||
Applied core fixes across several versions on this day.
|
||||
|
||||
- Fixed core issues in versions 1.0.10, 1.0.9, and 1.0.8
|
||||
@ -215,4 +231,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.
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/platformservice",
|
||||
"private": false,
|
||||
"version": "2.11.1",
|
||||
"private": true,
|
||||
"version": "2.8.6",
|
||||
"description": "A multifaceted platform service handling mail, SMS, letter delivery, and AI services.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
|
@ -2,16 +2,6 @@
|
||||
|
||||
## Latest Changes
|
||||
|
||||
### API Type Safety Improvements
|
||||
- [x] Create comprehensive TypeScript interfaces for all API methods
|
||||
- [x] Replace any types with specific interfaces in EmailService and MtaConnector
|
||||
- [x] Align interface types with their implementations
|
||||
- [x] Document all interface properties with detailed JSDoc comments
|
||||
- [x] Use type imports to ensure consistency between services
|
||||
- [x] Fix TypeScript build errors from interface placements
|
||||
- [x] Add proper method signatures for API endpoint handlers
|
||||
- [x] Implement type-safe API responses
|
||||
|
||||
### Test Stability Improvements
|
||||
- [x] Fix race conditions in SenderReputationMonitor tests
|
||||
- [x] Disable filesystem operations during tests to prevent race conditions
|
||||
@ -19,27 +9,6 @@
|
||||
- [x] Ensure all tests properly clean up shared resources
|
||||
- [x] Set consistent test environment with NODE_ENV=test
|
||||
|
||||
### Error Handling Improvements
|
||||
- [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
|
||||
- [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
|
||||
- [ ] Add context information to all log messages
|
||||
- [ ] Create consistent log levels and usage patterns
|
||||
- [ ] Add correlation IDs for request tracking
|
||||
- [ ] Implement log filtering and sampling options
|
||||
|
||||
### Mailgun Removal
|
||||
- [x] Remove Mailgun integration from keywords in package.json and npmextra.json
|
||||
- [x] Update EmailService comments to remove mentions of Mailgun
|
||||
|
@ -1,107 +0,0 @@
|
||||
# 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
|
@ -12,42 +12,7 @@ import { Email } from '../ts/mail/core/classes.email.js';
|
||||
let platformService: SzPlatformService;
|
||||
|
||||
tap.test('Setup test environment', async () => {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
platformService = new SzPlatformService();
|
||||
// Use start() instead of init() which doesn't exist
|
||||
await platformService.start();
|
||||
expect(platformService.mtaService).toBeTruthy();
|
||||
|
@ -1,367 +0,0 @@
|
||||
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();
|
@ -25,22 +25,8 @@ 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 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 platform service first
|
||||
const platformService = new SzPlatformService();
|
||||
|
||||
// Create a shared bounce manager
|
||||
const bounceManager = new BounceManager();
|
||||
@ -88,22 +74,8 @@ 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 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 platform service with default config
|
||||
const platformService = new SzPlatformService();
|
||||
|
||||
// Create MTA - don't await start() to avoid binding to ports
|
||||
platformService.mtaService = new MtaService(platformService, {
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/platformservice',
|
||||
version: '2.11.1',
|
||||
version: '2.8.6',
|
||||
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
|
||||
}
|
||||
|
@ -1,433 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
// 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
|
||||
};
|
@ -1,54 +0,0 @@
|
||||
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;
|
||||
};
|
||||
}
|
@ -1,770 +0,0 @@
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -1,86 +0,0 @@
|
||||
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;
|
||||
};
|
||||
}
|
@ -1,326 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,437 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,313 +0,0 @@
|
||||
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.`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,412 +0,0 @@
|
||||
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
|
||||
});
|
||||
};
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Platform Service Error Codes
|
||||
*
|
||||
* This file contains all error codes used across the platform service.
|
||||
*
|
||||
* Format: PREFIX_ERROR_TYPE
|
||||
* - PREFIX: Component/domain prefix (e.g., EMAIL, MTA, SMS)
|
||||
* - ERROR_TYPE: Specific error type within the domain
|
||||
*/
|
||||
|
||||
// General platform errors (PLATFORM_*)
|
||||
export const PLATFORM_INITIALIZATION_ERROR = 'PLATFORM_INITIALIZATION_ERROR';
|
||||
export const PLATFORM_CONFIGURATION_ERROR = 'PLATFORM_CONFIGURATION_ERROR';
|
||||
export const PLATFORM_OPERATION_ERROR = 'PLATFORM_OPERATION_ERROR';
|
||||
export const PLATFORM_NOT_IMPLEMENTED = 'PLATFORM_NOT_IMPLEMENTED';
|
||||
export const PLATFORM_NOT_SUPPORTED = 'PLATFORM_NOT_SUPPORTED';
|
||||
export const PLATFORM_SERVICE_UNAVAILABLE = 'PLATFORM_SERVICE_UNAVAILABLE';
|
||||
|
||||
// Email service errors (EMAIL_*)
|
||||
export const EMAIL_SERVICE_ERROR = 'EMAIL_SERVICE_ERROR';
|
||||
export const EMAIL_TEMPLATE_ERROR = 'EMAIL_TEMPLATE_ERROR';
|
||||
export const EMAIL_VALIDATION_ERROR = 'EMAIL_VALIDATION_ERROR';
|
||||
export const EMAIL_SEND_ERROR = 'EMAIL_SEND_ERROR';
|
||||
export const EMAIL_RECEIVE_ERROR = 'EMAIL_RECEIVE_ERROR';
|
||||
export const EMAIL_ATTACHMENT_ERROR = 'EMAIL_ATTACHMENT_ERROR';
|
||||
export const EMAIL_PARSE_ERROR = 'EMAIL_PARSE_ERROR';
|
||||
export const EMAIL_RATE_LIMIT_EXCEEDED = 'EMAIL_RATE_LIMIT_EXCEEDED';
|
||||
|
||||
// MTA-specific errors (MTA_*)
|
||||
export const MTA_CONNECTION_ERROR = 'MTA_CONNECTION_ERROR';
|
||||
export const MTA_AUTHENTICATION_ERROR = 'MTA_AUTHENTICATION_ERROR';
|
||||
export const MTA_DELIVERY_ERROR = 'MTA_DELIVERY_ERROR';
|
||||
export const MTA_CONFIGURATION_ERROR = 'MTA_CONFIGURATION_ERROR';
|
||||
export const MTA_DNS_ERROR = 'MTA_DNS_ERROR';
|
||||
export const MTA_TIMEOUT_ERROR = 'MTA_TIMEOUT_ERROR';
|
||||
export const MTA_PROTOCOL_ERROR = 'MTA_PROTOCOL_ERROR';
|
||||
|
||||
// Bounce management errors (BOUNCE_*)
|
||||
export const BOUNCE_PROCESSING_ERROR = 'BOUNCE_PROCESSING_ERROR';
|
||||
export const BOUNCE_STORAGE_ERROR = 'BOUNCE_STORAGE_ERROR';
|
||||
export const BOUNCE_CLASSIFICATION_ERROR = 'BOUNCE_CLASSIFICATION_ERROR';
|
||||
|
||||
// Email authentication errors (AUTH_*)
|
||||
export const AUTH_SPF_ERROR = 'AUTH_SPF_ERROR';
|
||||
export const AUTH_DKIM_ERROR = 'AUTH_DKIM_ERROR';
|
||||
export const AUTH_DMARC_ERROR = 'AUTH_DMARC_ERROR';
|
||||
export const AUTH_KEY_ERROR = 'AUTH_KEY_ERROR';
|
||||
|
||||
// Content scanning errors (SCAN_*)
|
||||
export const SCAN_ANALYSIS_ERROR = 'SCAN_ANALYSIS_ERROR';
|
||||
export const SCAN_MALWARE_DETECTED = 'SCAN_MALWARE_DETECTED';
|
||||
export const SCAN_PHISHING_DETECTED = 'SCAN_PHISHING_DETECTED';
|
||||
export const SCAN_CONTENT_REJECTED = 'SCAN_CONTENT_REJECTED';
|
||||
|
||||
// IP and reputation errors (REPUTATION_*)
|
||||
export const REPUTATION_CHECK_ERROR = 'REPUTATION_CHECK_ERROR';
|
||||
export const REPUTATION_DATA_ERROR = 'REPUTATION_DATA_ERROR';
|
||||
export const REPUTATION_BLOCKLIST_ERROR = 'REPUTATION_BLOCKLIST_ERROR';
|
||||
export const REPUTATION_UPDATE_ERROR = 'REPUTATION_UPDATE_ERROR';
|
||||
|
||||
// IP warmup errors (WARMUP_*)
|
||||
export const WARMUP_ALLOCATION_ERROR = 'WARMUP_ALLOCATION_ERROR';
|
||||
export const WARMUP_LIMIT_EXCEEDED = 'WARMUP_LIMIT_EXCEEDED';
|
||||
export const WARMUP_SCHEDULE_ERROR = 'WARMUP_SCHEDULE_ERROR';
|
||||
|
||||
// Network and connectivity errors (NETWORK_*)
|
||||
export const NETWORK_CONNECTION_ERROR = 'NETWORK_CONNECTION_ERROR';
|
||||
export const NETWORK_TIMEOUT = 'NETWORK_TIMEOUT';
|
||||
export const NETWORK_DNS_ERROR = 'NETWORK_DNS_ERROR';
|
||||
export const NETWORK_TLS_ERROR = 'NETWORK_TLS_ERROR';
|
||||
|
||||
// Queue and processing errors (QUEUE_*)
|
||||
export const QUEUE_FULL_ERROR = 'QUEUE_FULL_ERROR';
|
||||
export const QUEUE_PROCESSING_ERROR = 'QUEUE_PROCESSING_ERROR';
|
||||
export const QUEUE_PERSISTENCE_ERROR = 'QUEUE_PERSISTENCE_ERROR';
|
||||
export const QUEUE_ITEM_NOT_FOUND = 'QUEUE_ITEM_NOT_FOUND';
|
||||
|
||||
// DcRouter errors (DCR_*)
|
||||
export const DCR_ROUTING_ERROR = 'DCR_ROUTING_ERROR';
|
||||
export const DCR_CONFIGURATION_ERROR = 'DCR_CONFIGURATION_ERROR';
|
||||
export const DCR_PROXY_ERROR = 'DCR_PROXY_ERROR';
|
||||
export const DCR_DOMAIN_ERROR = 'DCR_DOMAIN_ERROR';
|
||||
|
||||
// SMS service errors (SMS_*)
|
||||
export const SMS_SERVICE_ERROR = 'SMS_SERVICE_ERROR';
|
||||
export const SMS_SEND_ERROR = 'SMS_SEND_ERROR';
|
||||
export const SMS_VALIDATION_ERROR = 'SMS_VALIDATION_ERROR';
|
||||
export const SMS_RATE_LIMIT_EXCEEDED = 'SMS_RATE_LIMIT_EXCEEDED';
|
||||
|
||||
// Storage errors (STORAGE_*)
|
||||
export const STORAGE_WRITE_ERROR = 'STORAGE_WRITE_ERROR';
|
||||
export const STORAGE_READ_ERROR = 'STORAGE_READ_ERROR';
|
||||
export const STORAGE_DELETE_ERROR = 'STORAGE_DELETE_ERROR';
|
||||
export const STORAGE_QUOTA_EXCEEDED = 'STORAGE_QUOTA_EXCEEDED';
|
||||
|
||||
// Rule management errors (RULE_*)
|
||||
export const RULE_VALIDATION_ERROR = 'RULE_VALIDATION_ERROR';
|
||||
export const RULE_EXECUTION_ERROR = 'RULE_EXECUTION_ERROR';
|
||||
export const RULE_NOT_FOUND = 'RULE_NOT_FOUND';
|
||||
|
||||
// Type definitions for error severity
|
||||
export enum ErrorSeverity {
|
||||
/** Critical errors that require immediate attention */
|
||||
CRITICAL = 'CRITICAL',
|
||||
|
||||
/** High-impact errors that may affect service functioning */
|
||||
HIGH = 'HIGH',
|
||||
|
||||
/** Medium-impact errors that cause partial degradation */
|
||||
MEDIUM = 'MEDIUM',
|
||||
|
||||
/** Low-impact errors that have minimal or local impact */
|
||||
LOW = 'LOW',
|
||||
|
||||
/** Informational errors that are not problematic */
|
||||
INFO = 'INFO'
|
||||
}
|
||||
|
||||
// Type definitions for error categories
|
||||
export enum ErrorCategory {
|
||||
/** Errors related to configuration */
|
||||
CONFIGURATION = 'CONFIGURATION',
|
||||
|
||||
/** Errors related to network connectivity */
|
||||
CONNECTIVITY = 'CONNECTIVITY',
|
||||
|
||||
/** Errors related to authentication/authorization */
|
||||
AUTHENTICATION = 'AUTHENTICATION',
|
||||
|
||||
/** Errors related to data validation */
|
||||
VALIDATION = 'VALIDATION',
|
||||
|
||||
/** Errors related to resource availability */
|
||||
RESOURCE = 'RESOURCE',
|
||||
|
||||
/** Errors related to service operations */
|
||||
OPERATION = 'OPERATION',
|
||||
|
||||
/** Errors related to third-party integrations */
|
||||
INTEGRATION = 'INTEGRATION',
|
||||
|
||||
/** Errors related to security */
|
||||
SECURITY = 'SECURITY',
|
||||
|
||||
/** Errors related to data storage */
|
||||
STORAGE = 'STORAGE',
|
||||
|
||||
/** Errors that don't fit into other categories */
|
||||
OTHER = 'OTHER'
|
||||
}
|
||||
|
||||
// Type definitions for error recoverability
|
||||
export enum ErrorRecoverability {
|
||||
/** Error cannot be automatically recovered from */
|
||||
NON_RECOVERABLE = 'NON_RECOVERABLE',
|
||||
|
||||
/** Error might be recoverable with retry */
|
||||
MAYBE_RECOVERABLE = 'MAYBE_RECOVERABLE',
|
||||
|
||||
/** Error is definitely recoverable with retries */
|
||||
RECOVERABLE = 'RECOVERABLE',
|
||||
|
||||
/** Error is transient and should resolve without action */
|
||||
TRANSIENT = 'TRANSIENT'
|
||||
}
|
@ -1,193 +0,0 @@
|
||||
/**
|
||||
* 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!;
|
||||
}
|
@ -1,611 +0,0 @@
|
||||
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.`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,352 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -2,7 +2,4 @@ export * from './00_commitinfo_data.js';
|
||||
import { SzPlatformService } from './platformservice.js';
|
||||
export * from './mail/index.js';
|
||||
|
||||
// DcRouter
|
||||
export * from './classes.dcrouter.js';
|
||||
|
||||
export const runCli = async () => {}
|
86
ts/logger.ts
86
ts/logger.ts
@ -1,91 +1,9 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
// 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({
|
||||
export const logger = new plugins.smartlog.Smartlog({
|
||||
logContext: {
|
||||
environment: envMap[nodeEnv] || 'production',
|
||||
environment: '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();
|
||||
|
@ -5,10 +5,6 @@ import { logger } from '../../logger.js';
|
||||
// Import MTA classes
|
||||
import { MtaService } from './classes.mta.js';
|
||||
import { Email as MtaEmail } from '../core/classes.email.js';
|
||||
import { DeliveryStatus } from './classes.emailsendjob.js';
|
||||
|
||||
// Re-export for use in index.ts
|
||||
export { DeliveryStatus };
|
||||
|
||||
// Import Email types
|
||||
export interface IEmailOptions {
|
||||
@ -23,6 +19,14 @@ export interface IEmailOptions {
|
||||
headers?: { [key: string]: string };
|
||||
}
|
||||
|
||||
// Reuse the DeliveryStatus from the email send job
|
||||
export enum DeliveryStatus {
|
||||
PENDING = 'pending',
|
||||
PROCESSING = 'processing',
|
||||
DELIVERED = 'delivered',
|
||||
DEFERRED = 'deferred',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
// Reuse the IAttachment interface
|
||||
export interface IAttachment {
|
||||
@ -33,66 +37,6 @@ export interface IAttachment {
|
||||
encoding?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email status details
|
||||
*/
|
||||
export interface IEmailStatusDetails {
|
||||
/** Number of delivery attempts */
|
||||
attempts?: number;
|
||||
/** Timestamp of last delivery attempt */
|
||||
lastAttempt?: Date;
|
||||
/** Timestamp of next scheduled attempt */
|
||||
nextAttempt?: Date;
|
||||
/** Error message if delivery failed */
|
||||
error?: string;
|
||||
/** Message explaining the status */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email status response
|
||||
*/
|
||||
export interface IEmailStatusResponse {
|
||||
/** Current status of the email */
|
||||
status: DeliveryStatus | 'unknown' | 'error';
|
||||
/** Additional status details */
|
||||
details?: IEmailStatusDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for sending an email via MTA
|
||||
*/
|
||||
export interface ISendEmailOptions {
|
||||
/** Whether to use MIME format conversion */
|
||||
useMimeFormat?: boolean;
|
||||
/** Whether to track clicks */
|
||||
trackClicks?: boolean;
|
||||
/** Whether to track opens */
|
||||
trackOpens?: boolean;
|
||||
/** Message priority (1-5, where 1 is highest) */
|
||||
priority?: number;
|
||||
/** Message scheduling options */
|
||||
schedule?: {
|
||||
/** Time to send the email */
|
||||
sendAt?: Date | string;
|
||||
/** Time the message expires */
|
||||
expireAt?: Date | string;
|
||||
};
|
||||
/** DKIM signing options */
|
||||
dkim?: {
|
||||
/** Whether to sign the message */
|
||||
sign?: boolean;
|
||||
/** Domain to use for signing */
|
||||
domain?: string;
|
||||
/** Key selector to use */
|
||||
selector?: string;
|
||||
};
|
||||
/** Additional headers */
|
||||
headers?: Record<string, string>;
|
||||
/** Message tags for categorization */
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export class MtaConnector {
|
||||
public emailRef: EmailService;
|
||||
private mtaService: MtaService;
|
||||
@ -102,13 +46,6 @@ export class MtaConnector {
|
||||
this.mtaService = mtaService || this.emailRef.mtaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email using the MTA service
|
||||
* @param smartmail The email to send
|
||||
* @param toAddresses Recipients (comma-separated or array)
|
||||
* @param options Additional options
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send an email using the MTA service
|
||||
* @param smartmail The email to send
|
||||
@ -118,7 +55,7 @@ export class MtaConnector {
|
||||
public async sendEmail(
|
||||
smartmail: plugins.smartmail.Smartmail<any>,
|
||||
toAddresses: string | string[],
|
||||
options: ISendEmailOptions = {}
|
||||
options: any = {}
|
||||
): Promise<string> {
|
||||
// Check if recipients are on the suppression list
|
||||
const recipients = Array.isArray(toAddresses)
|
||||
@ -164,7 +101,7 @@ export class MtaConnector {
|
||||
const emailOptions: Record<string, any> = { ...options };
|
||||
|
||||
// Check if we should use MIME format
|
||||
const useMimeFormat = options.useMimeFormat !== false; // Default to true
|
||||
const useMimeFormat = options.useMimeFormat ?? true;
|
||||
|
||||
if (useMimeFormat) {
|
||||
// Use smartmail's MIME conversion for improved handling
|
||||
@ -584,28 +521,28 @@ export class MtaConnector {
|
||||
/**
|
||||
* Check the status of a sent email
|
||||
* @param emailId The email ID to check
|
||||
* @returns Current status and details
|
||||
*/
|
||||
public async checkEmailStatus(emailId: string): Promise<IEmailStatusResponse> {
|
||||
public async checkEmailStatus(emailId: string): Promise<{
|
||||
status: string;
|
||||
details?: any;
|
||||
}> {
|
||||
try {
|
||||
const status = this.mtaService.getEmailStatus(emailId);
|
||||
|
||||
if (!status) {
|
||||
return {
|
||||
status: 'unknown' as const,
|
||||
status: 'unknown',
|
||||
details: { message: 'Email not found' }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
// Use type assertion to ensure this passes type check
|
||||
status: status.status as DeliveryStatus,
|
||||
status: status.status,
|
||||
details: {
|
||||
attempts: status.attempts,
|
||||
lastAttempt: status.lastAttempt,
|
||||
nextAttempt: status.nextAttempt,
|
||||
error: status.error?.message,
|
||||
message: `Status: ${status.status}${status.error ? `, Error: ${status.error.message}` : ''}`
|
||||
error: status.error?.message
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@ -617,7 +554,7 @@ export class MtaConnector {
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'error' as const,
|
||||
status: 'error',
|
||||
details: { message: error.message }
|
||||
};
|
||||
}
|
||||
|
@ -65,18 +65,8 @@ export class ApiManager {
|
||||
new plugins.typedrequest.TypedHandler('checkEmailStatus', async (requestData) => {
|
||||
// If MTA is enabled, use it to check status
|
||||
if (this.emailRef.mtaConnector) {
|
||||
const detailedStatus = await this.emailRef.mtaConnector.checkEmailStatus(requestData.emailId);
|
||||
|
||||
// Convert to the expected API response format
|
||||
const apiResponse: plugins.servezoneInterfaces.platformservice.mta.IReq_CheckEmailStatus['response'] = {
|
||||
status: detailedStatus.status.toString(), // Convert enum to string
|
||||
details: {
|
||||
message: detailedStatus.details?.message ||
|
||||
(detailedStatus.details?.error ? `Error: ${detailedStatus.details.error}` :
|
||||
`Status: ${detailedStatus.status}`)
|
||||
}
|
||||
};
|
||||
return apiResponse;
|
||||
const status = await this.emailRef.mtaConnector.checkEmailStatus(requestData.emailId);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Status tracking not available if MTA is not configured
|
||||
|
@ -10,111 +10,18 @@ import { logger } from '../../logger.js';
|
||||
import type { SzPlatformService } from '../../platformservice.js';
|
||||
|
||||
// Import MTA service
|
||||
import { MtaService } from '../delivery/classes.mta.js';
|
||||
import { MtaService, type IMtaConfig } from '../delivery/classes.mta.js';
|
||||
|
||||
// Import configuration interfaces
|
||||
import type { IEmailConfig } from '../../config/email.config.js';
|
||||
import { ConfigValidator, emailConfigSchema } from '../../config/index.js';
|
||||
|
||||
/**
|
||||
* Options for sending an email
|
||||
* @see ISendEmailOptions in MtaConnector
|
||||
*/
|
||||
export type ISendEmailOptions = import('../delivery/classes.connector.mta.js').ISendEmailOptions;
|
||||
|
||||
/**
|
||||
* Template context data for email templates
|
||||
* @see ITemplateContext in TemplateManager
|
||||
*/
|
||||
export type ITemplateContext = import('../core/classes.templatemanager.js').ITemplateContext;
|
||||
|
||||
/**
|
||||
* Validation options for email addresses
|
||||
* Compatible with EmailValidator.validate options
|
||||
*/
|
||||
export interface IValidateEmailOptions {
|
||||
/** Check MX records for the domain */
|
||||
checkMx?: boolean;
|
||||
/** Check if the domain is disposable (temporary email) */
|
||||
checkDisposable?: boolean;
|
||||
/** Check if the email is a role account (e.g., info@, support@) */
|
||||
checkRole?: boolean;
|
||||
/** Only check syntax without DNS lookups */
|
||||
checkSyntaxOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of email validation
|
||||
* @see IEmailValidationResult from EmailValidator
|
||||
*/
|
||||
export type IValidationResult = import('../core/classes.emailvalidator.js').IEmailValidationResult;
|
||||
|
||||
/**
|
||||
* Email service statistics
|
||||
*/
|
||||
export interface IEmailServiceStats {
|
||||
/** Active email providers */
|
||||
activeProviders: string[];
|
||||
/** MTA statistics, if MTA is active */
|
||||
mta?: {
|
||||
/** Service start time */
|
||||
startTime: Date;
|
||||
/** Total emails received */
|
||||
emailsReceived: number;
|
||||
/** Total emails sent */
|
||||
emailsSent: number;
|
||||
/** Total emails that failed to send */
|
||||
emailsFailed: number;
|
||||
/** Active SMTP connections */
|
||||
activeConnections: number;
|
||||
/** Current email queue size */
|
||||
queueSize: number;
|
||||
/** Certificate information */
|
||||
certificateInfo?: {
|
||||
/** Domain for the certificate */
|
||||
domain: string;
|
||||
/** Certificate expiration date */
|
||||
expiresAt: Date;
|
||||
/** Days until certificate expires */
|
||||
daysUntilExpiry: number;
|
||||
};
|
||||
/** IP warmup information */
|
||||
warmupInfo?: {
|
||||
/** Whether IP warmup is enabled */
|
||||
enabled: boolean;
|
||||
/** Number of active IPs */
|
||||
activeIPs: number;
|
||||
/** Number of IPs in warmup phase */
|
||||
inWarmupPhase: number;
|
||||
/** Number of IPs that completed warmup */
|
||||
completedWarmup: number;
|
||||
};
|
||||
/** Reputation monitoring information */
|
||||
reputationInfo?: {
|
||||
/** Whether reputation monitoring is enabled */
|
||||
enabled: boolean;
|
||||
/** Number of domains being monitored */
|
||||
monitoredDomains: number;
|
||||
/** Average reputation score across domains */
|
||||
averageScore: number;
|
||||
/** Number of domains with reputation issues */
|
||||
domainsWithIssues: number;
|
||||
};
|
||||
/** Rate limiting information */
|
||||
rateLimiting?: {
|
||||
/** Global rate limit statistics */
|
||||
global: {
|
||||
/** Current available tokens */
|
||||
availableTokens: number;
|
||||
/** Maximum tokens per period */
|
||||
maxTokens: number;
|
||||
/** Current consumption rate */
|
||||
consumptionRate: number;
|
||||
/** Number of rate limiting events */
|
||||
rateLimitEvents: number;
|
||||
};
|
||||
};
|
||||
export interface IEmailConstructorOptions {
|
||||
useMta?: boolean;
|
||||
mtaConfig?: IMtaConfig;
|
||||
templateConfig?: {
|
||||
from?: string;
|
||||
replyTo?: string;
|
||||
footerHtml?: string;
|
||||
footerText?: string;
|
||||
};
|
||||
loadTemplatesFromDir?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,21 +48,19 @@ export class EmailService {
|
||||
public bounceManager: BounceManager;
|
||||
|
||||
// configuration
|
||||
private config: IEmailConfig;
|
||||
private config: IEmailConstructorOptions;
|
||||
|
||||
constructor(platformServiceRefArg: SzPlatformService, options: IEmailConfig = {}) {
|
||||
constructor(platformServiceRefArg: SzPlatformService, options: IEmailConstructorOptions = {}) {
|
||||
this.platformServiceRef = platformServiceRefArg;
|
||||
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
|
||||
// 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;
|
||||
// Set default options
|
||||
this.config = {
|
||||
useMta: options.useMta ?? true,
|
||||
mtaConfig: options.mtaConfig || {},
|
||||
templateConfig: options.templateConfig || {},
|
||||
loadTemplatesFromDir: options.loadTemplatesFromDir ?? true
|
||||
};
|
||||
|
||||
// Initialize validator
|
||||
this.emailValidator = new EmailValidator();
|
||||
@ -231,7 +136,7 @@ export class EmailService {
|
||||
public async sendEmail(
|
||||
email: plugins.smartmail.Smartmail<any>,
|
||||
to: string | string[],
|
||||
options: ISendEmailOptions = {}
|
||||
options: any = {}
|
||||
): Promise<string> {
|
||||
// Determine which connector to use
|
||||
if (this.config.useMta && this.mtaConnector) {
|
||||
@ -251,8 +156,8 @@ export class EmailService {
|
||||
public async sendTemplateEmail(
|
||||
templateId: string,
|
||||
to: string | string[],
|
||||
context: ITemplateContext = {},
|
||||
options: ISendEmailOptions = {}
|
||||
context: any = {},
|
||||
options: any = {}
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Get email from template
|
||||
@ -278,35 +183,28 @@ export class EmailService {
|
||||
*/
|
||||
public async validateEmail(
|
||||
email: string,
|
||||
options: IValidateEmailOptions = {}
|
||||
): Promise<IValidationResult> {
|
||||
options: {
|
||||
checkMx?: boolean;
|
||||
checkDisposable?: boolean;
|
||||
checkRole?: boolean;
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
return this.emailValidator.validate(email, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email service statistics
|
||||
* @returns Service statistics in the format expected by the API
|
||||
*/
|
||||
public getStats(): plugins.servezoneInterfaces.platformservice.mta.IReq_GetEMailStats['response'] {
|
||||
// First generate detailed internal stats
|
||||
const detailedStats: IEmailServiceStats = {
|
||||
public getStats() {
|
||||
const stats: any = {
|
||||
activeProviders: []
|
||||
};
|
||||
|
||||
if (this.config.useMta) {
|
||||
detailedStats.activeProviders.push('mta');
|
||||
detailedStats.mta = this.mtaService.getStats();
|
||||
stats.activeProviders.push('mta');
|
||||
stats.mta = this.mtaService.getStats();
|
||||
}
|
||||
|
||||
// Convert detailed stats to the format expected by the API
|
||||
const apiStats: plugins.servezoneInterfaces.platformservice.mta.IReq_GetEMailStats['response'] = {
|
||||
totalEmailsSent: detailedStats.mta?.emailsSent || 0,
|
||||
totalEmailsDelivered: detailedStats.mta?.emailsSent || 0, // Default to emails sent if we don't track delivery separately
|
||||
totalEmailsBounced: detailedStats.mta?.emailsFailed || 0,
|
||||
averageDeliveryTimeMs: 0, // We don't track this yet
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
return apiStats;
|
||||
return stats;
|
||||
}
|
||||
}
|
@ -25,11 +25,6 @@ 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
|
||||
|
@ -4,9 +4,6 @@ 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;
|
||||
@ -21,168 +18,28 @@ export class SzPlatformService {
|
||||
public mtaService: MtaService;
|
||||
public smsService: SmsService;
|
||||
|
||||
// Platform configuration
|
||||
public config: IPlatformConfig;
|
||||
|
||||
/**
|
||||
* 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
|
||||
public async start() {
|
||||
this.platformserviceDb = new PlatformServiceDb(this);
|
||||
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||
|
||||
// Initialize database
|
||||
this.platformserviceDb = new PlatformServiceDb(this);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// lets start the sub services
|
||||
this.emailService = new EmailService(this);
|
||||
this.mtaService = new MtaService(this);
|
||||
this.smsService = new SmsService(this, {
|
||||
apiGatewayApiToken: apiToken,
|
||||
...this.config.sms
|
||||
apiGatewayApiToken: await this.serviceQenv.getEnvVarOnDemand('SMS_API_TOKEN'),
|
||||
});
|
||||
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
|
||||
// lets start the server finally
|
||||
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
|
||||
cors: true,
|
||||
});
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
@ -55,9 +55,6 @@ 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';
|
||||
|
||||
|
@ -3,29 +3,19 @@ import * as paths from '../paths.js';
|
||||
import { logger } from '../logger.js';
|
||||
import type { SzPlatformService } from '../platformservice.js';
|
||||
|
||||
import type { ISmsConfig } from '../config/sms.config.js';
|
||||
import { ConfigValidator, smsConfigSchema } from '../config/index.js';
|
||||
export interface ISmsConstructorOptions {
|
||||
apiGatewayApiToken: string;
|
||||
}
|
||||
|
||||
export class SmsService {
|
||||
public platformServiceRef: SzPlatformService;
|
||||
public projectinfo: plugins.projectinfo.ProjectInfo;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
public config: ISmsConfig;
|
||||
public options: ISmsConstructorOptions;
|
||||
|
||||
constructor(platformServiceRefArg: SzPlatformService, options: ISmsConfig) {
|
||||
constructor(platformServiceRefArg: SzPlatformService, optionsArg: ISmsConstructorOptions) {
|
||||
this.platformServiceRef = platformServiceRefArg;
|
||||
|
||||
// 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.options = optionsArg;
|
||||
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
}
|
||||
|
||||
@ -63,11 +53,8 @@ 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,
|
||||
sender: fromName,
|
||||
message: messageText,
|
||||
recipients: [{ msisdn: toNumber }],
|
||||
};
|
||||
@ -76,7 +63,7 @@ export class SmsService {
|
||||
method: 'POST',
|
||||
requestBody: JSON.stringify(payload),
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${this.config.apiGatewayApiToken}:`).toString('base64')}`,
|
||||
Authorization: `Basic ${Buffer.from(`${this.options.apiGatewayApiToken}:`).toString('base64')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/platformservice',
|
||||
version: '2.11.1',
|
||||
version: '2.8.6',
|
||||
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user