@@ -41,7 +41,6 @@ export class AcmeCA {
4141 } ) ;
4242 }
4343
44- private pendingAcmeChallenges : { [ token : string ] : string | undefined } = { }
4544 private pendingCertRenewals : { [ domain : string ] : ( Promise < AcmeGeneratedCertificate > & { id : string } ) | undefined } = { } ;
4645
4746 getChallengeResponse ( token : string ) {
@@ -158,8 +157,13 @@ export class AcmeCA {
158157 return ;
159158 }
160159
160+ // Only Google Trust Services supports short-lived certificates
161+ const requestCert = this . acmeProvider === 'google'
162+ ? this . requestShortLivedCertificate ( domain , { attemptId } )
163+ : this . requestNewCertificate ( domain , { attemptId } ) ;
164+
161165 const refreshPromise = Object . assign (
162- this . requestNewCertificate ( domain , { attemptId } ) . then ( ( certData ) => {
166+ requestCert . then ( ( certData ) => {
163167 delete this . pendingCertRenewals [ cacheKey ] ;
164168 this . certCache . cacheCert ( { ...certData , domain : cacheKey } ) ;
165169 console . log ( `Expired-mode cert issued for ${ domain } (${ attemptId } ), will expire: ${ new Date ( certData . expiry ) . toISOString ( ) } ` ) ;
@@ -270,33 +274,79 @@ export class AcmeCA {
270274 termsOfServiceAgreed : true ,
271275 email : 'contact@' + domain ,
272276 skipChallengeVerification : true ,
273- challengeCreateFn : async ( _authz , challenge , keyAuth ) => {
274- if ( challenge . type !== 'http-01' ) {
275- throw new Error ( `Unexpected ${ challenge . type } challenge (${ options . attemptId } )` ) ;
276- }
277+ challengeCreateFn : async ( ) => {
278+ // Challenge responses are stateless - getChallengeResponse() computes
279+ // the key authorization directly from the token
280+ } ,
281+ challengeRemoveFn : async ( ) => { }
282+ } ) ;
277283
278- console . log ( `Preparing for ${ challenge . type } ACME challenge ${ challenge . token } (${ options . attemptId } )` ) ;
284+ console . log ( `Successfully ACMEd new certificate for ${ domain } (${ options . attemptId } )` ) ;
279285
280- this . pendingAcmeChallenges [ challenge . token ] = keyAuth ;
281- } ,
282- challengeRemoveFn : async ( _authz , challenge ) => {
283- if ( challenge . type !== 'http-01' ) {
284- throw new Error ( `Unexpected ${ challenge . type } challenge ( ${ options . attemptId } )` ) ;
285- }
286+ return {
287+ key : key . toString ( ) ,
288+ cert ,
289+ expiry : ( new Date ( ACME . crypto . readCertificateInfo ( cert ) . notAfter ) ) . valueOf ( )
290+ } ;
291+ }
286292
287- console . log ( `Removing ACME ${
288- challenge . status
289- } ${
290- challenge . type
291- } challenge ${
292- JSON . stringify ( challenge )
293- } ) (${ options . attemptId } )`) ;
293+ /**
294+ * Request a short-lived certificate (1 day validity) using the lower-level ACME API.
295+ * Google Trust Services supports validity periods as short as 1 day.
296+ */
297+ private async requestShortLivedCertificate ( domain : string , options : { attemptId : string } ) : Promise < AcmeGeneratedCertificate > {
298+ console . log ( `Requesting short-lived certificate for ${ domain } (${ options . attemptId } )` ) ;
294299
295- delete this . pendingAcmeChallenges [ challenge . token ] ;
296- }
300+ const [ key , csr ] = await ACME . crypto . createCsr ( {
301+ commonName : domain
297302 } ) ;
298303
299- console . log ( `Successfully ACMEd new certificate for ${ domain } (${ options . attemptId } )` ) ;
304+ // Ensure account exists
305+ await this . acmeClient . createAccount ( {
306+ termsOfServiceAgreed : true ,
307+ contact : [ `mailto:contact@${ domain } ` ]
308+ } ) ;
309+
310+ // Create order with 1-day validity
311+ const notBefore = new Date ( ) ;
312+ const notAfter = new Date ( Date . now ( ) + ONE_DAY ) ;
313+
314+ console . log ( `Creating order with validity: ${ notBefore . toISOString ( ) } to ${ notAfter . toISOString ( ) } (${ options . attemptId } )` ) ;
315+
316+ const order = await this . acmeClient . createOrder ( {
317+ identifiers : [ { type : 'dns' , value : domain } ] ,
318+ notBefore : notBefore . toISOString ( ) ,
319+ notAfter : notAfter . toISOString ( )
320+ } ) ;
321+
322+ // Get and complete authorizations
323+ const authorizations = await this . acmeClient . getAuthorizations ( order ) ;
324+ console . log ( `Got ${ authorizations . length } authorizations for ${ domain } (${ options . attemptId } )` ) ;
325+
326+ for ( const authz of authorizations ) {
327+ if ( authz . status === 'valid' ) {
328+ console . log ( `Authorization already valid for ${ authz . identifier . value } (${ options . attemptId } )` ) ;
329+ continue ;
330+ }
331+
332+ // Find http-01 challenge
333+ const challenge = authz . challenges . find ( c => c . type === 'http-01' ) ;
334+ if ( ! challenge ) {
335+ throw new Error ( `No http-01 challenge found for ${ authz . identifier . value } (${ options . attemptId } )` ) ;
336+ }
337+
338+ // Complete challenge - response is stateless via getChallengeResponse()
339+ console . log ( `Completing http-01 challenge for ${ authz . identifier . value } (${ options . attemptId } )` ) ;
340+ await this . acmeClient . completeChallenge ( challenge ) ;
341+ await this . acmeClient . waitForValidStatus ( challenge ) ;
342+ }
343+
344+ // Finalize order and get certificate
345+ console . log ( `Finalizing order for ${ domain } (${ options . attemptId } )` ) ;
346+ const finalized = await this . acmeClient . finalizeOrder ( order , csr ) ;
347+ const cert = await this . acmeClient . getCertificate ( finalized ) ;
348+
349+ console . log ( `Successfully issued short-lived certificate for ${ domain } (${ options . attemptId } )` ) ;
300350
301351 return {
302352 key : key . toString ( ) ,
0 commit comments