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:
@ -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({});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user