BREAKING CHANGE(config): convert configuration management to read-only; remove updateConfiguration endpoint and client-side editing
This commit is contained in:
241
ts/cache/documents/classes.cached.dkim.ts
vendored
Normal file
241
ts/cache/documents/classes.cached.dkim.ts
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { CachedDocument, TTL } from '../classes.cached.document.js';
|
||||
import { CacheDb } from '../classes.cachedb.js';
|
||||
|
||||
/**
|
||||
* Helper to get the smartdata database instance
|
||||
*/
|
||||
const getDb = () => CacheDb.getInstance().getDb();
|
||||
|
||||
/**
|
||||
* CachedDKIMKey - Stores DKIM key pairs for email signing
|
||||
*
|
||||
* Caches DKIM private/public key pairs per domain and selector.
|
||||
* Default TTL is 90 days (typical key rotation interval).
|
||||
*/
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class CachedDKIMKey extends CachedDocument<CachedDKIMKey> {
|
||||
/**
|
||||
* Composite key: domain:selector
|
||||
*/
|
||||
@plugins.smartdata.unI()
|
||||
@plugins.smartdata.svDb()
|
||||
public domainSelector: string;
|
||||
|
||||
/**
|
||||
* Domain for this DKIM key
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public domain: string;
|
||||
|
||||
/**
|
||||
* DKIM selector (e.g., 'mta', 'default', '2024')
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public selector: string;
|
||||
|
||||
/**
|
||||
* Private key in PEM format
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public privateKey: string;
|
||||
|
||||
/**
|
||||
* Public key in PEM format
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public publicKey: string;
|
||||
|
||||
/**
|
||||
* Public key for DNS TXT record (base64, no headers)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public publicKeyDns: string;
|
||||
|
||||
/**
|
||||
* Key size in bits (e.g., 1024, 2048)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public keySize: number = 2048;
|
||||
|
||||
/**
|
||||
* Key algorithm (e.g., 'rsa-sha256')
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public algorithm: string = 'rsa-sha256';
|
||||
|
||||
/**
|
||||
* When the key was generated
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public generatedAt: Date;
|
||||
|
||||
/**
|
||||
* When the key was last rotated
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public rotatedAt: Date;
|
||||
|
||||
/**
|
||||
* Previous selector (for key rotation)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public previousSelector: string;
|
||||
|
||||
/**
|
||||
* Number of emails signed with this key
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public signCount: number = 0;
|
||||
|
||||
/**
|
||||
* Whether this key is currently active
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public isActive: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setTTL(TTL.DAYS_90); // Default 90-day TTL
|
||||
this.generatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the composite key from domain and selector
|
||||
*/
|
||||
public static createDomainSelector(domain: string, selector: string): string {
|
||||
return `${domain.toLowerCase()}:${selector.toLowerCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DKIM key entry
|
||||
*/
|
||||
public static createNew(domain: string, selector: string): CachedDKIMKey {
|
||||
const key = new CachedDKIMKey();
|
||||
key.domain = domain.toLowerCase();
|
||||
key.selector = selector.toLowerCase();
|
||||
key.domainSelector = CachedDKIMKey.createDomainSelector(domain, selector);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by domain and selector
|
||||
*/
|
||||
public static async findByDomainSelector(
|
||||
domain: string,
|
||||
selector: string
|
||||
): Promise<CachedDKIMKey | null> {
|
||||
const domainSelector = CachedDKIMKey.createDomainSelector(domain, selector);
|
||||
return await CachedDKIMKey.getInstance({
|
||||
domainSelector,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all keys for a domain
|
||||
*/
|
||||
public static async findByDomain(domain: string): Promise<CachedDKIMKey[]> {
|
||||
return await CachedDKIMKey.getInstances({
|
||||
domain: domain.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the active key for a domain
|
||||
*/
|
||||
public static async findActiveForDomain(domain: string): Promise<CachedDKIMKey | null> {
|
||||
const keys = await CachedDKIMKey.getInstances({
|
||||
domain: domain.toLowerCase(),
|
||||
isActive: true,
|
||||
});
|
||||
return keys.length > 0 ? keys[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all active keys
|
||||
*/
|
||||
public static async findAllActive(): Promise<CachedDKIMKey[]> {
|
||||
return await CachedDKIMKey.getInstances({
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key pair
|
||||
*/
|
||||
public setKeyPair(privateKey: string, publicKey: string, publicKeyDns?: string): void {
|
||||
this.privateKey = privateKey;
|
||||
this.publicKey = publicKey;
|
||||
this.publicKeyDns = publicKeyDns || this.extractPublicKeyDns(publicKey);
|
||||
this.generatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the base64 public key for DNS from PEM format
|
||||
*/
|
||||
private extractPublicKeyDns(publicKeyPem: string): string {
|
||||
// Remove PEM headers and newlines
|
||||
return publicKeyPem
|
||||
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
||||
.replace(/-----END PUBLIC KEY-----/g, '')
|
||||
.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the DNS TXT record value
|
||||
*/
|
||||
public getDnsTxtRecord(): string {
|
||||
return `v=DKIM1; k=rsa; p=${this.publicKeyDns}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full DNS record name
|
||||
*/
|
||||
public getDnsRecordName(): string {
|
||||
return `${this.selector}._domainkey.${this.domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that this key was used to sign an email
|
||||
*/
|
||||
public recordSign(): void {
|
||||
this.signCount++;
|
||||
this.touch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate this key (e.g., during rotation)
|
||||
*/
|
||||
public deactivate(): void {
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate this key
|
||||
*/
|
||||
public activate(): void {
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate to a new selector
|
||||
*/
|
||||
public rotate(newSelector: string): void {
|
||||
this.previousSelector = this.selector;
|
||||
this.selector = newSelector.toLowerCase();
|
||||
this.domainSelector = CachedDKIMKey.createDomainSelector(this.domain, this.selector);
|
||||
this.rotatedAt = new Date();
|
||||
this.signCount = 0;
|
||||
// Reset TTL on rotation
|
||||
this.setTTL(TTL.DAYS_90);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key needs rotation (based on age or sign count)
|
||||
*/
|
||||
public needsRotation(maxAgeDays: number = 90, maxSignCount: number = 1000000): boolean {
|
||||
const ageMs = Date.now() - this.generatedAt.getTime();
|
||||
const ageDays = ageMs / (24 * 60 * 60 * 1000);
|
||||
return ageDays > maxAgeDays || this.signCount > maxSignCount;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user