11import * as crypto from 'node:crypto' ;
22import * as ACME from 'acme-client' ;
3+ import { MaybePromise } from '@httptoolkit/util' ;
4+
35import { PersistentCertCache } from './cert-cache.js' ;
46
57const ONE_MINUTE = 1000 * 60 ;
@@ -34,7 +36,7 @@ export class AcmeCA {
3436 }
3537
3638 private pendingAcmeChallenges : { [ token : string ] : string | undefined } = { }
37- private pendingCertRenewals : { [ domain : string ] : Promise < AcmeGeneratedCertificate > | undefined } = { } ;
39+ private pendingCertRenewals : { [ domain : string ] : ( Promise < AcmeGeneratedCertificate > & { id : string } ) | undefined } = { } ;
3840
3941 private readonly acmeClient = ACME . crypto . createPrivateKey ( ) . then (
4042 ( accountKey ) => new ACME . Client ( {
@@ -73,10 +75,6 @@ export class AcmeCA {
7375 domain : string ,
7476 options : { forceRegenerate ?: boolean , attemptId : string }
7577 ) : Promise < AcmeGeneratedCertificate > {
76- if ( ! options . attemptId ) {
77- options . attemptId = Math . random ( ) . toString ( 16 ) . slice ( 2 ) ;
78- }
79-
8078 const cachedCert = this . certCache . getCert ( domain ) ;
8179 if ( cachedCert && ! options . forceRegenerate ) {
8280 // If we have this cert in the cache, we generally want to use that.
@@ -89,17 +87,24 @@ export class AcmeCA {
8987 }
9088
9189 if (
92- cachedCert . expiry - Date . now ( ) < ONE_WEEK && // Expires soon
93- ! this . pendingCertRenewals [ domain ] // Not already updating
90+ cachedCert . expiry - Date . now ( ) < ONE_WEEK // Expires soon
9491 ) {
95- // Not yet expired, but expiring soon - we want to refresh this certificate, but
96- // we're OK to do it async and keep using the current one for now.
97- console . log ( `Renewing near-expiry certificate for ${ domain } (${ options . attemptId } )` ) ;
98-
99- this . pendingCertRenewals [ domain ] = this . getCertificate ( domain , {
100- forceRegenerate : true ,
101- attemptId : options . attemptId
102- } ) ;
92+ if ( ! this . pendingCertRenewals [ domain ] ) {
93+ // Not yet expired, but expiring soon - we want to refresh this certificate, but
94+ // we're OK to do it async and keep using the current one for now.
95+ console . log ( `Renewing near-expiry certificate for ${ domain } (${ options . attemptId } )` ) ;
96+
97+ this . pendingCertRenewals [ domain ] = Object . assign ( this . getCertificate ( domain , {
98+ forceRegenerate : true ,
99+ attemptId : options . attemptId
100+ } ) , { id : options . attemptId } ) ;
101+ } else {
102+ console . log ( `Certificate refresh already pending for ${ domain } (${ options . attemptId } ) from attempt ${
103+ this . pendingCertRenewals [ domain ] ! . id
104+ } `) ;
105+ }
106+ } else {
107+ console . log ( `Cached cert still valid for ${ domain } (${ options . attemptId } )` ) ;
103108 }
104109
105110 return cachedCert ;
@@ -109,32 +114,41 @@ export class AcmeCA {
109114 else if ( options . forceRegenerate ) console . log ( `Force regenerating cert for ${ domain } (${ options . attemptId } )` ) ;
110115
111116 if ( this . pendingCertRenewals [ domain ] && ! options . forceRegenerate ) {
117+ console . log ( `Certificate generation already pending for ${ domain } (${ options . attemptId } ) from attempt ${
118+ this . pendingCertRenewals [ domain ] ! . id
119+ } `) ;
120+
112121 // Coalesce updates for pending certs into one
113122 return this . pendingCertRenewals [ domain ] ! ;
114123 }
115124
116- const refreshPromise : Promise < AcmeGeneratedCertificate > = this . requestNewCertificate ( domain , {
125+ const refreshPromise = Object . assign ( this . requestNewCertificate ( domain , {
117126 attemptId : options . attemptId
118- } ) . then ( ( certData ) => {
127+ } ) . then ( ( certData ) : MaybePromise < AcmeGeneratedCertificate > => {
119128 if (
120129 this . pendingCertRenewals [ domain ] &&
121130 this . pendingCertRenewals [ domain ] !== refreshPromise
122131 ) {
132+ console . log ( `Certificate generation for ${ domain } (${ options . attemptId } ) superseded by another attempt ${
133+ this . pendingCertRenewals [ domain ] ! . id
134+ } `) ;
135+
123136 // Don't think this should happen, but if we're somehow ever not the current cert
124137 // update, delegate to the 'real' cert update instead.
125138 return this . pendingCertRenewals [ domain ] ! ;
126139 }
127140
128141 delete this . pendingCertRenewals [ domain ] ;
129142 this . certCache . cacheCert ( { ...certData , domain } ) ;
130- console . log ( `Cert refresh completed for domain ${ domain } (${ options . attemptId } ), hash:${ crypto . hash ( 'sha256' , certData . cert ) } ` ) ;
143+ console . log ( `Cert generation completed for domain ${ domain } (${ options . attemptId } ), hash:${ crypto . hash ( 'sha256' , certData . cert ) } ` ) ;
131144 return certData ;
132145 } ) . catch ( ( e ) => {
133- console . log ( `Cert refresh failed (${ options . attemptId } )` , e ) ;
146+ console . log ( `Cert generation failed (${ options . attemptId } )` , e ) ;
134147 return this . getCertificate ( domain , { forceRegenerate : true , attemptId : options . attemptId } ) ;
135- } ) ;
148+ } ) , { id : options . attemptId } ) ;
136149
137150 this . pendingCertRenewals [ domain ] = refreshPromise ;
151+ console . log ( `Started cert generation for domain ${ domain } (${ options . attemptId } )` ) ;
138152 return refreshPromise ;
139153 }
140154
0 commit comments