BREAKING CHANGE(SmartAcme (Cert Management)): Refactor certificate management and challenge handling API to use a unified certManager interface, remove legacy storage, and update challenge workflows.

This commit is contained in:
2025-04-30 17:27:17 +00:00
parent 6a53346d14
commit 6363ec4be6
12 changed files with 235 additions and 175 deletions

View File

@ -1,8 +1,8 @@
import * as plugins from './smartacme.plugins.js';
import { SmartacmeCert } from './smartacme.classes.cert.js';
import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
import type { ICertManager } from './interfaces/certmanager.js';
import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
import { commitinfo } from './00_commitinfo_data.js';
import { SmartacmeCert } from './smartacme.classes.cert.js';
/**
* the options for the class @see SmartAcme
@ -10,7 +10,10 @@ import { commitinfo } from './00_commitinfo_data.js';
export interface ISmartAcmeOptions {
accountPrivateKey?: string;
accountEmail: string;
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
/**
* Certificate storage manager (e.g., Mongo or in-memory).
*/
certManager: ICertManager;
// Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
environment: 'production' | 'integration';
/**
@ -59,8 +62,8 @@ export class SmartAcme {
private privateKey: string;
// certmanager
private certmanager: SmartacmeCertManager;
// certificate manager for persistence (implements ICertManager)
private certmanager: ICertManager;
private certmatcher: SmartacmeCertMatcher;
// retry/backoff configuration (resolved with defaults)
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
@ -78,10 +81,10 @@ export class SmartAcme {
this.logger.enableConsole();
// initialize retry/backoff options
this.retryOptions = {
retries: optionsArg.retryOptions?.retries ?? 3,
factor: optionsArg.retryOptions?.factor ?? 2,
retries: optionsArg.retryOptions?.retries ?? 10,
factor: optionsArg.retryOptions?.factor ?? 4,
minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 60000,
};
// initialize challenge handlers (must provide at least one)
if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
@ -107,10 +110,11 @@ export class SmartAcme {
this.privateKey =
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
// CertMangaer
this.certmanager = new SmartacmeCertManager(this, {
mongoDescriptor: this.options.mongoDescriptor,
});
// Initialize certificate manager
if (!this.options.certManager) {
throw new Error('You must provide a certManager via options.certManager');
}
this.certmanager = this.options.certManager;
await this.certmanager.init();
// CertMatcher
@ -138,9 +142,14 @@ export class SmartAcme {
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
}
public async stop() {
await this.certmanager.smartdataDb.close();
/**
* Stops the SmartAcme instance and closes certificate store connections.
*/
public async stop() {
if (this.certmanager && typeof (this.certmanager as any).close === 'function') {
await (this.certmanager as any).close();
}
}
/** Retry helper with exponential backoff */
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
let attempt = 0;
@ -221,7 +230,8 @@ export class SmartAcme {
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
return retrievedCertificate;
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
await retrievedCertificate.delete();
// Remove old certificate via certManager
await this.certmanager.deleteCertificate(certDomainName);
}
// lets make sure others get the same interest
@ -277,15 +287,45 @@ export class SmartAcme {
}
this.pendingChallenges.push(input);
try {
// Prepare the challenge (set DNS record, write file, etc.)
await this.retry(() => handler.prepare(input), `${type}.prepare`);
if (handler.verify) {
await this.retry(() => handler.verify!(input), `${type}.verify`);
} else {
await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
// For DNS-01, wait for propagation before verification
if (type === 'dns-01') {
const dnsInput = input as { hostName: string; challenge: string };
// Wait for authoritative DNS propagation before ACME verify
await this.retry(
() => this.smartdns.checkUntilAvailable(dnsInput.hostName, 'TXT', dnsInput.challenge, 100, 5000),
`${type}.propagation`,
);
// Extra cool-down to ensure ACME server sees the new TXT record
this.logger.log('info', 'Cooling down for 1 minute before ACME verification');
await plugins.smartdelay.delayFor(60000);
}
// Official ACME verification (ensures challenge is publicly reachable)
await this.retry(
() => this.client.verifyChallenge(authz, selectedChallengeArg),
`${type}.verifyChallenge`,
);
// Notify ACME server to complete the challenge
await this.retry(
() => this.client.completeChallenge(selectedChallengeArg),
`${type}.completeChallenge`,
);
// Wait for valid status (warnings on staging timeouts)
try {
await this.retry(
() => this.client.waitForValidStatus(selectedChallengeArg),
`${type}.waitForValidStatus`,
);
} catch (err) {
await this.logger.log(
'warn',
`Challenge ${type} did not reach valid status in time, proceeding to finalize`,
err,
);
}
await this.retry(() => this.client.completeChallenge(selectedChallengeArg), `${type}.completeChallenge`);
await this.retry(() => this.client.waitForValidStatus(selectedChallengeArg), `${type}.waitForValidStatus`);
} finally {
// Always cleanup resource
try {
await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
} catch (err) {
@ -307,19 +347,17 @@ export class SmartAcme {
/* Done */
await this.certmanager.storeCertificate({
// Store the new certificate record
const certRecord = new SmartacmeCert({
id: plugins.smartunique.shortId(),
domainName: certDomainName,
privateKey: key.toString(),
publicKey: cert.toString(),
csr: csr.toString(),
created: Date.now(),
validUntil:
Date.now() +
plugins.smarttime.getMilliSecondsFromUnits({
days: 90,
}),
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
});
await this.certmanager.storeCertificate(certRecord);
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
currentDomainInterst.fullfillInterest(newCertificate);
@ -327,7 +365,4 @@ export class SmartAcme {
return newCertificate;
}
public async getAllCertificates(): Promise<SmartacmeCert[]> {
return SmartacmeCert.getInstances({});
}
}