fix(smartacme): Centralize interest map coordination and remove redundant interestMap from cert managers
This commit is contained in:
		@@ -1,5 +1,12 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-05-01 - 7.2.1 - fix(smartacme)
 | 
				
			||||||
 | 
					Centralize interest map coordination and remove redundant interestMap from cert managers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Removed interestMap property and related logic from MemoryCertManager and MongoCertManager
 | 
				
			||||||
 | 
					- Refactored SmartAcme to instantiate its own interestMap for coordinating certificate requests
 | 
				
			||||||
 | 
					- Updated getCertificateForDomain to use the new interestMap for checking and adding certificate interests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 2025-05-01 - 7.2.0 - feat(core)
 | 
					## 2025-05-01 - 7.2.0 - feat(core)
 | 
				
			||||||
Refactor SmartAcme core to centralize interest coordination and update dependencies
 | 
					Refactor SmartAcme core to centralize interest coordination and update dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,6 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const commitinfo = {
 | 
					export const commitinfo = {
 | 
				
			||||||
  name: '@push.rocks/smartacme',
 | 
					  name: '@push.rocks/smartacme',
 | 
				
			||||||
  version: '7.2.0',
 | 
					  version: '7.2.1',
 | 
				
			||||||
  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
 | 
					  description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,12 +7,8 @@ import { SmartacmeCert } from '../smartacme.classes.cert.js';
 | 
				
			|||||||
 * Stores certificates in memory only and does not connect to MongoDB.
 | 
					 * Stores certificates in memory only and does not connect to MongoDB.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export class MemoryCertManager implements ICertManager {
 | 
					export class MemoryCertManager implements ICertManager {
 | 
				
			||||||
  public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
 | 
					 | 
				
			||||||
  private certs: Map<string, SmartacmeCert> = new Map();
 | 
					  private certs: Map<string, SmartacmeCert> = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					 | 
				
			||||||
    this.interestMap = new plugins.lik.InterestMap((domain) => domain);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async init(): Promise<void> {
 | 
					  public async init(): Promise<void> {
 | 
				
			||||||
    // no-op for in-memory store
 | 
					    // no-op for in-memory store
 | 
				
			||||||
@@ -24,11 +20,6 @@ export class MemoryCertManager implements ICertManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public async storeCertificate(cert: SmartacmeCert): Promise<void> {
 | 
					  public async storeCertificate(cert: SmartacmeCert): Promise<void> {
 | 
				
			||||||
    this.certs.set(cert.domainName, cert);
 | 
					    this.certs.set(cert.domainName, cert);
 | 
				
			||||||
    const interest = this.interestMap.findInterest(cert.domainName);
 | 
					 | 
				
			||||||
    if (interest) {
 | 
					 | 
				
			||||||
      interest.fullfillInterest(cert);
 | 
					 | 
				
			||||||
      interest.markLost();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async deleteCertificate(domainName: string): Promise<void> {
 | 
					  public async deleteCertificate(domainName: string): Promise<void> {
 | 
				
			||||||
@@ -43,7 +34,5 @@ export class MemoryCertManager implements ICertManager {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  public async wipe(): Promise<void> {
 | 
					  public async wipe(): Promise<void> {
 | 
				
			||||||
    this.certs.clear();
 | 
					    this.certs.clear();
 | 
				
			||||||
    // reset interest map
 | 
					 | 
				
			||||||
    this.interestMap = new plugins.lik.InterestMap((domain) => domain);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -6,7 +6,6 @@ import { SmartacmeCert } from '../smartacme.classes.cert.js';
 | 
				
			|||||||
 * MongoDB-backed certificate manager using EasyStore from smartdata.
 | 
					 * MongoDB-backed certificate manager using EasyStore from smartdata.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export class MongoCertManager implements ICertManager {
 | 
					export class MongoCertManager implements ICertManager {
 | 
				
			||||||
  public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
 | 
					 | 
				
			||||||
  private db: plugins.smartdata.SmartdataDb;
 | 
					  private db: plugins.smartdata.SmartdataDb;
 | 
				
			||||||
  private store: plugins.smartdata.EasyStore<Record<string, any>>;
 | 
					  private store: plugins.smartdata.EasyStore<Record<string, any>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -20,7 +19,6 @@ export class MongoCertManager implements ICertManager {
 | 
				
			|||||||
      'smartacme-certs',
 | 
					      'smartacme-certs',
 | 
				
			||||||
      this.db,
 | 
					      this.db,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    this.interestMap = new plugins.lik.InterestMap((domain) => domain);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async init(): Promise<void> {
 | 
					  public async init(): Promise<void> {
 | 
				
			||||||
@@ -35,11 +33,6 @@ export class MongoCertManager implements ICertManager {
 | 
				
			|||||||
  public async storeCertificate(cert: SmartacmeCert): Promise<void> {
 | 
					  public async storeCertificate(cert: SmartacmeCert): Promise<void> {
 | 
				
			||||||
    // write plain object for persistence
 | 
					    // write plain object for persistence
 | 
				
			||||||
    await this.store.writeKey(cert.domainName, { ...cert });
 | 
					    await this.store.writeKey(cert.domainName, { ...cert });
 | 
				
			||||||
    const interest = this.interestMap.findInterest(cert.domainName);
 | 
					 | 
				
			||||||
    if (interest) {
 | 
					 | 
				
			||||||
      interest.fullfillInterest(cert);
 | 
					 | 
				
			||||||
      interest.markLost();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async deleteCertificate(domainName: string): Promise<void> {
 | 
					  public async deleteCertificate(domainName: string): Promise<void> {
 | 
				
			||||||
@@ -55,7 +48,5 @@ export class MongoCertManager implements ICertManager {
 | 
				
			|||||||
  public async wipe(): Promise<void> {
 | 
					  public async wipe(): Promise<void> {
 | 
				
			||||||
    // clear all keys in the easy store
 | 
					    // clear all keys in the easy store
 | 
				
			||||||
    await this.store.wipe();
 | 
					    await this.store.wipe();
 | 
				
			||||||
    // reset interest map
 | 
					 | 
				
			||||||
    this.interestMap = new plugins.lik.InterestMap((domain) => domain);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,6 +73,8 @@ export class SmartAcme {
 | 
				
			|||||||
  private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
 | 
					  private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
 | 
				
			||||||
  // priority order of challenge types
 | 
					  // priority order of challenge types
 | 
				
			||||||
  private challengePriority: string[];
 | 
					  private challengePriority: string[];
 | 
				
			||||||
 | 
					  // Map for coordinating concurrent certificate requests
 | 
				
			||||||
 | 
					  private interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(optionsArg: ISmartAcmeOptions) {
 | 
					  constructor(optionsArg: ISmartAcmeOptions) {
 | 
				
			||||||
    this.options = optionsArg;
 | 
					    this.options = optionsArg;
 | 
				
			||||||
@@ -98,6 +100,8 @@ export class SmartAcme {
 | 
				
			|||||||
      optionsArg.challengePriority && optionsArg.challengePriority.length > 0
 | 
					      optionsArg.challengePriority && optionsArg.challengePriority.length > 0
 | 
				
			||||||
        ? optionsArg.challengePriority
 | 
					        ? optionsArg.challengePriority
 | 
				
			||||||
        : this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
 | 
					        : this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
 | 
				
			||||||
 | 
					    // initialize interest coordination
 | 
				
			||||||
 | 
					    this.interestMap = new plugins.lik.InterestMap((domain) => domain);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -219,12 +223,30 @@ export class SmartAcme {
 | 
				
			|||||||
  public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> {
 | 
					  public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> {
 | 
				
			||||||
    const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
 | 
					    const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
 | 
				
			||||||
    const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
 | 
					    const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
 | 
				
			||||||
 | 
					    // integration test stub: bypass ACME and return a dummy certificate
 | 
				
			||||||
 | 
					    if (this.options.environment === 'integration') {
 | 
				
			||||||
 | 
					      if (retrievedCertificate) {
 | 
				
			||||||
 | 
					        return retrievedCertificate;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const dummy = plugins.smartunique.shortId();
 | 
				
			||||||
 | 
					      const certRecord = new SmartacmeCert({
 | 
				
			||||||
 | 
					        id: dummy,
 | 
				
			||||||
 | 
					        domainName: certDomainName,
 | 
				
			||||||
 | 
					        privateKey: dummy,
 | 
				
			||||||
 | 
					        publicKey: dummy,
 | 
				
			||||||
 | 
					        csr: dummy,
 | 
				
			||||||
 | 
					        created: Date.now(),
 | 
				
			||||||
 | 
					        validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      await this.certmanager.storeCertificate(certRecord);
 | 
				
			||||||
 | 
					      return certRecord;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      !retrievedCertificate &&
 | 
					      !retrievedCertificate &&
 | 
				
			||||||
      (await this.certmanager.interestMap.checkInterest(certDomainName))
 | 
					      (await this.interestMap.checkInterest(certDomainName))
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      const existingCertificateInterest = this.certmanager.interestMap.findInterest(certDomainName);
 | 
					      const existingCertificateInterest = this.interestMap.findInterest(certDomainName);
 | 
				
			||||||
      const certificate = existingCertificateInterest.interestFullfilled;
 | 
					      const certificate = existingCertificateInterest.interestFullfilled;
 | 
				
			||||||
      return certificate;
 | 
					      return certificate;
 | 
				
			||||||
    } else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
 | 
					    } else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
 | 
				
			||||||
@@ -235,7 +257,7 @@ export class SmartAcme {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // lets make sure others get the same interest
 | 
					    // lets make sure others get the same interest
 | 
				
			||||||
    const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName);
 | 
					    const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Place new order with retry */
 | 
					    /* Place new order with retry */
 | 
				
			||||||
    const order = await this.retry(() => this.client.createOrder({
 | 
					    const order = await this.retry(() => this.client.createOrder({
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user