feat(integration): components now play nicer with each other

This commit is contained in:
2025-05-30 05:30:06 +00:00
parent 2c244c4a9a
commit 40db395591
19 changed files with 2849 additions and 264 deletions

View File

@ -13,11 +13,31 @@ export interface IKeyPaths {
publicKeyPath: string;
}
export interface IDkimKeyMetadata {
domain: string;
selector: string;
createdAt: number;
rotatedAt?: number;
previousSelector?: string;
keySize: number;
}
export class DKIMCreator {
private keysDir: string;
private storageManager?: any; // StorageManager instance
constructor(keysDir = paths.keysDir) {
constructor(keysDir = paths.keysDir, storageManager?: any) {
this.keysDir = keysDir;
this.storageManager = storageManager;
// If no storage manager provided, log warning
if (!storageManager) {
console.warn(
'⚠️ WARNING: DKIMCreator initialized without StorageManager.\n' +
' DKIM keys will only be stored to filesystem.\n' +
' Consider passing a StorageManager instance for better storage flexibility.'
);
}
}
public async getKeyPathsForDomain(domainArg: string): Promise<IKeyPaths> {
@ -45,19 +65,63 @@ export class DKIMCreator {
await this.handleDKIMKeysForDomain(domain);
}
// Read DKIM keys from disk
// Read DKIM keys - always use storage manager, migrate from filesystem if needed
public async readDKIMKeys(domainArg: string): Promise<{ privateKey: string; publicKey: string }> {
const keyPaths = await this.getKeyPathsForDomain(domainArg);
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
readFile(keyPaths.privateKeyPath),
readFile(keyPaths.publicKeyPath),
]);
// Try to read from storage manager first
if (this.storageManager) {
try {
const [privateKey, publicKey] = await Promise.all([
this.storageManager.get(`/email/dkim/${domainArg}/private.key`),
this.storageManager.get(`/email/dkim/${domainArg}/public.key`)
]);
if (privateKey && publicKey) {
return { privateKey, publicKey };
}
} catch (error) {
// Fall through to migration check
}
// Check if keys exist in filesystem and migrate them to storage manager
const keyPaths = await this.getKeyPathsForDomain(domainArg);
try {
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
readFile(keyPaths.privateKeyPath),
readFile(keyPaths.publicKeyPath),
]);
// Convert the buffers to strings
const privateKey = privateKeyBuffer.toString();
const publicKey = publicKeyBuffer.toString();
// Convert the buffers to strings
const privateKey = privateKeyBuffer.toString();
const publicKey = publicKeyBuffer.toString();
// Migrate to storage manager
console.log(`Migrating DKIM keys for ${domainArg} from filesystem to StorageManager`);
await Promise.all([
this.storageManager.set(`/email/dkim/${domainArg}/private.key`, privateKey),
this.storageManager.set(`/email/dkim/${domainArg}/public.key`, publicKey)
]);
return { privateKey, publicKey };
} catch (error) {
if (error.code === 'ENOENT') {
// Keys don't exist anywhere
throw new Error(`DKIM keys not found for domain ${domainArg}`);
}
throw error;
}
} else {
// No storage manager, use filesystem directly
const keyPaths = await this.getKeyPathsForDomain(domainArg);
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
readFile(keyPaths.privateKeyPath),
readFile(keyPaths.publicKeyPath),
]);
return { privateKey, publicKey };
const privateKey = privateKeyBuffer.toString();
const publicKey = publicKeyBuffer.toString();
return { privateKey, publicKey };
}
}
// Create a DKIM key pair - changed to public for API access
@ -71,13 +135,27 @@ export class DKIMCreator {
return { privateKey, publicKey };
}
// Store a DKIM key pair to disk - changed to public for API access
// Store a DKIM key pair - uses storage manager if available, else disk
public async storeDKIMKeys(
privateKey: string,
publicKey: string,
privateKeyPath: string,
publicKeyPath: string
): Promise<void> {
// Store in storage manager if available
if (this.storageManager) {
// Extract domain from path (e.g., /path/to/keys/example.com-private.pem -> example.com)
const match = privateKeyPath.match(/\/([^\/]+)-private\.pem$/);
if (match) {
const domain = match[1];
await Promise.all([
this.storageManager.set(`/email/dkim/${domain}/private.key`, privateKey),
this.storageManager.set(`/email/dkim/${domain}/public.key`, publicKey)
]);
}
}
// Also store to filesystem for backward compatibility
await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]);
}
@ -117,4 +195,246 @@ export class DKIMCreator {
value: dnsRecordValue,
};
}
/**
* Get DKIM key metadata for a domain
*/
private async getKeyMetadata(domain: string, selector: string = 'default'): Promise<IDkimKeyMetadata | null> {
if (!this.storageManager) {
return null;
}
const metadataKey = `/email/dkim/${domain}/${selector}/metadata`;
const metadataStr = await this.storageManager.get(metadataKey);
if (!metadataStr) {
return null;
}
return JSON.parse(metadataStr) as IDkimKeyMetadata;
}
/**
* Save DKIM key metadata
*/
private async saveKeyMetadata(metadata: IDkimKeyMetadata): Promise<void> {
if (!this.storageManager) {
return;
}
const metadataKey = `/email/dkim/${metadata.domain}/${metadata.selector}/metadata`;
await this.storageManager.set(metadataKey, JSON.stringify(metadata));
}
/**
* Check if DKIM keys need rotation
*/
public async needsRotation(domain: string, selector: string = 'default', rotationIntervalDays: number = 90): Promise<boolean> {
const metadata = await this.getKeyMetadata(domain, selector);
if (!metadata) {
// No metadata means old keys, should rotate
return true;
}
const now = Date.now();
const keyAgeMs = now - metadata.createdAt;
const keyAgeDays = keyAgeMs / (1000 * 60 * 60 * 24);
return keyAgeDays >= rotationIntervalDays;
}
/**
* Rotate DKIM keys for a domain
*/
public async rotateDkimKeys(domain: string, currentSelector: string = 'default', keySize: number = 2048): Promise<string> {
console.log(`Rotating DKIM keys for ${domain}...`);
// Generate new selector based on date
const now = new Date();
const newSelector = `key${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
// Create new keys with custom key size
const { privateKey, publicKey } = await generateKeyPair('rsa', {
modulusLength: keySize,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
});
// Store new keys with new selector
const newKeyPaths = await this.getKeyPathsForSelector(domain, newSelector);
// Store in storage manager if available
if (this.storageManager) {
await Promise.all([
this.storageManager.set(`/email/dkim/${domain}/${newSelector}/private.key`, privateKey),
this.storageManager.set(`/email/dkim/${domain}/${newSelector}/public.key`, publicKey)
]);
}
// Also store to filesystem
await this.storeDKIMKeys(
privateKey,
publicKey,
newKeyPaths.privateKeyPath,
newKeyPaths.publicKeyPath
);
// Save metadata for new keys
const metadata: IDkimKeyMetadata = {
domain,
selector: newSelector,
createdAt: Date.now(),
previousSelector: currentSelector,
keySize
};
await this.saveKeyMetadata(metadata);
// Update metadata for old keys
const oldMetadata = await this.getKeyMetadata(domain, currentSelector);
if (oldMetadata) {
oldMetadata.rotatedAt = Date.now();
await this.saveKeyMetadata(oldMetadata);
}
console.log(`DKIM keys rotated for ${domain}. New selector: ${newSelector}`);
return newSelector;
}
/**
* Get key paths for a specific selector
*/
public async getKeyPathsForSelector(domain: string, selector: string): Promise<IKeyPaths> {
return {
privateKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-private.pem`),
publicKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-public.pem`),
};
}
/**
* Read DKIM keys for a specific selector
*/
public async readDKIMKeysForSelector(domain: string, selector: string): Promise<{ privateKey: string; publicKey: string }> {
// Try to read from storage manager first
if (this.storageManager) {
try {
const [privateKey, publicKey] = await Promise.all([
this.storageManager.get(`/email/dkim/${domain}/${selector}/private.key`),
this.storageManager.get(`/email/dkim/${domain}/${selector}/public.key`)
]);
if (privateKey && publicKey) {
return { privateKey, publicKey };
}
} catch (error) {
// Fall through to migration check
}
// Check if keys exist in filesystem and migrate them to storage manager
const keyPaths = await this.getKeyPathsForSelector(domain, selector);
try {
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
readFile(keyPaths.privateKeyPath),
readFile(keyPaths.publicKeyPath),
]);
const privateKey = privateKeyBuffer.toString();
const publicKey = publicKeyBuffer.toString();
// Migrate to storage manager
console.log(`Migrating DKIM keys for ${domain}/${selector} from filesystem to StorageManager`);
await Promise.all([
this.storageManager.set(`/email/dkim/${domain}/${selector}/private.key`, privateKey),
this.storageManager.set(`/email/dkim/${domain}/${selector}/public.key`, publicKey)
]);
return { privateKey, publicKey };
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`DKIM keys not found for domain ${domain} with selector ${selector}`);
}
throw error;
}
} else {
// No storage manager, use filesystem directly
const keyPaths = await this.getKeyPathsForSelector(domain, selector);
const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
readFile(keyPaths.privateKeyPath),
readFile(keyPaths.publicKeyPath),
]);
const privateKey = privateKeyBuffer.toString();
const publicKey = publicKeyBuffer.toString();
return { privateKey, publicKey };
}
}
/**
* Get DNS record for a specific selector
*/
public async getDNSRecordForSelector(domain: string, selector: string): Promise<plugins.tsclass.network.IDnsRecord> {
const keys = await this.readDKIMKeysForSelector(domain, selector);
// Remove the PEM header and footer and newlines
const pemHeader = '-----BEGIN PUBLIC KEY-----';
const pemFooter = '-----END PUBLIC KEY-----';
const keyContents = keys.publicKey
.replace(pemHeader, '')
.replace(pemFooter, '')
.replace(/\n/g, '');
// Generate the DKIM DNS TXT record
const dnsRecordValue = `v=DKIM1; h=sha256; k=rsa; p=${keyContents}`;
return {
name: `${selector}._domainkey.${domain}`,
type: 'TXT',
dnsSecEnabled: null,
value: dnsRecordValue,
};
}
/**
* Clean up old DKIM keys after grace period
*/
public async cleanupOldKeys(domain: string, gracePeriodDays: number = 30): Promise<void> {
if (!this.storageManager) {
return;
}
// List all selectors for the domain
const metadataKeys = await this.storageManager.list(`/email/dkim/${domain}/`);
for (const key of metadataKeys) {
if (key.endsWith('/metadata')) {
const metadataStr = await this.storageManager.get(key);
if (metadataStr) {
const metadata = JSON.parse(metadataStr) as IDkimKeyMetadata;
// Check if key is rotated and past grace period
if (metadata.rotatedAt) {
const gracePeriodMs = gracePeriodDays * 24 * 60 * 60 * 1000;
const now = Date.now();
if (now - metadata.rotatedAt > gracePeriodMs) {
console.log(`Cleaning up old DKIM keys for ${domain} selector ${metadata.selector}`);
// Delete key files
const keyPaths = await this.getKeyPathsForSelector(domain, metadata.selector);
try {
await plugins.fs.promises.unlink(keyPaths.privateKeyPath);
await plugins.fs.promises.unlink(keyPaths.publicKeyPath);
} catch (error) {
console.warn(`Failed to delete old key files: ${error.message}`);
}
// Delete metadata
await this.storageManager.delete(key);
}
}
}
}
}
}
}