2025-11-18 19:34:26 +00:00
|
|
|
/**
|
|
|
|
|
* SQLite-based Certificate Manager for SmartACME
|
|
|
|
|
*
|
2025-11-25 23:27:27 +00:00
|
|
|
* Implements ICertManager interface to store SSL certificates directly in SQLite database.
|
|
|
|
|
* Certificate PEM content is stored in the database, not on the filesystem.
|
2025-11-18 19:34:26 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as plugins from '../plugins.ts';
|
|
|
|
|
import { logger } from '../logging.ts';
|
2025-11-25 04:20:19 +00:00
|
|
|
import { getErrorMessage } from '../utils/error.ts';
|
2025-11-18 19:34:26 +00:00
|
|
|
import { OneboxDatabase } from './database.ts';
|
|
|
|
|
|
|
|
|
|
export class SqliteCertManager implements plugins.smartacme.ICertManager {
|
|
|
|
|
private database: OneboxDatabase;
|
|
|
|
|
|
2025-11-25 23:27:27 +00:00
|
|
|
constructor(database: OneboxDatabase) {
|
2025-11-18 19:34:26 +00:00
|
|
|
this.database = database;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize the certificate manager
|
|
|
|
|
*/
|
|
|
|
|
async init(): Promise<void> {
|
2025-11-25 23:27:27 +00:00
|
|
|
logger.info('Certificate manager initialized (database storage)');
|
2025-11-18 19:34:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieve a certificate by domain name
|
|
|
|
|
*/
|
|
|
|
|
async retrieveCertificate(domainName: string): Promise<plugins.smartacme.Cert | null> {
|
|
|
|
|
try {
|
|
|
|
|
const dbCert = this.database.getSSLCertificate(domainName);
|
|
|
|
|
|
2025-11-25 23:27:27 +00:00
|
|
|
if (!dbCert || !dbCert.keyPem || !dbCert.fullchainPem) {
|
2025-11-18 19:34:26 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert database format to SmartacmeCert format
|
|
|
|
|
const cert = new plugins.smartacme.Cert({
|
|
|
|
|
id: dbCert.id?.toString() || domainName,
|
|
|
|
|
domainName: dbCert.domain,
|
|
|
|
|
created: dbCert.createdAt,
|
2025-11-25 23:27:27 +00:00
|
|
|
privateKey: dbCert.keyPem,
|
|
|
|
|
publicKey: dbCert.fullchainPem, // Full chain as public key
|
2025-11-18 19:34:26 +00:00
|
|
|
csr: '', // CSR not stored separately
|
|
|
|
|
validUntil: dbCert.expiryDate,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return cert;
|
|
|
|
|
} catch (error) {
|
2025-11-25 04:20:19 +00:00
|
|
|
logger.warn(`Failed to retrieve certificate for ${domainName}: ${getErrorMessage(error)}`);
|
2025-11-18 19:34:26 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Store a certificate
|
|
|
|
|
*/
|
|
|
|
|
async storeCertificate(cert: plugins.smartacme.Cert): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const domain = cert.domainName;
|
|
|
|
|
|
|
|
|
|
// Extract certificate from full chain (first certificate in the chain)
|
2025-11-25 23:27:27 +00:00
|
|
|
const certPem = this.extractCertFromChain(cert.publicKey);
|
2025-11-18 19:34:26 +00:00
|
|
|
|
2025-11-25 23:27:27 +00:00
|
|
|
// Store/update in database with PEM content
|
2025-11-18 19:34:26 +00:00
|
|
|
const existing = this.database.getSSLCertificate(domain);
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
this.database.updateSSLCertificate(domain, {
|
2025-11-25 23:27:27 +00:00
|
|
|
certPem: certPem,
|
|
|
|
|
keyPem: cert.privateKey,
|
|
|
|
|
fullchainPem: cert.publicKey,
|
2025-11-18 19:34:26 +00:00
|
|
|
expiryDate: cert.validUntil,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await this.database.createSSLCertificate({
|
|
|
|
|
domain,
|
2025-11-25 23:27:27 +00:00
|
|
|
certPem: certPem,
|
|
|
|
|
keyPem: cert.privateKey,
|
|
|
|
|
fullchainPem: cert.publicKey,
|
2025-11-18 19:34:26 +00:00
|
|
|
expiryDate: cert.validUntil,
|
2025-11-25 23:27:27 +00:00
|
|
|
issuer: "Let's Encrypt",
|
2025-11-18 19:34:26 +00:00
|
|
|
createdAt: cert.created,
|
|
|
|
|
updatedAt: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.success(`Certificate stored for ${domain}`);
|
|
|
|
|
} catch (error) {
|
2025-11-25 04:20:19 +00:00
|
|
|
logger.error(`Failed to store certificate for ${cert.domainName}: ${getErrorMessage(error)}`);
|
2025-11-18 19:34:26 +00:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a certificate
|
|
|
|
|
*/
|
|
|
|
|
async deleteCertificate(domainName: string): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const dbCert = this.database.getSSLCertificate(domainName);
|
|
|
|
|
|
|
|
|
|
if (dbCert) {
|
|
|
|
|
// Delete from database
|
|
|
|
|
this.database.deleteSSLCertificate(domainName);
|
|
|
|
|
logger.info(`Certificate deleted for ${domainName}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-11-25 04:20:19 +00:00
|
|
|
logger.error(`Failed to delete certificate for ${domainName}: ${getErrorMessage(error)}`);
|
2025-11-18 19:34:26 +00:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Close the certificate manager
|
|
|
|
|
*/
|
|
|
|
|
async close(): Promise<void> {
|
|
|
|
|
// SQLite database is managed by OneboxDatabase, nothing to close here
|
|
|
|
|
logger.info('Certificate manager closed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wipe all certificates (for testing)
|
|
|
|
|
*/
|
|
|
|
|
async wipe(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const certs = this.database.getAllSSLCertificates();
|
|
|
|
|
|
|
|
|
|
for (const cert of certs) {
|
|
|
|
|
await this.deleteCertificate(cert.domain);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn('All certificates wiped');
|
|
|
|
|
} catch (error) {
|
2025-11-25 04:20:19 +00:00
|
|
|
logger.error(`Failed to wipe certificates: ${getErrorMessage(error)}`);
|
2025-11-18 19:34:26 +00:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract the first certificate from a PEM chain
|
|
|
|
|
*/
|
|
|
|
|
private extractCertFromChain(fullChain: string): string {
|
|
|
|
|
const certMatch = fullChain.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/);
|
|
|
|
|
return certMatch ? certMatch[0] : fullChain;
|
|
|
|
|
}
|
|
|
|
|
}
|