feat(smartacme): Implement exponential backoff retry logic and graceful shutdown handling in SmartAcme; update acme-client dependency to v5.4.0
This commit is contained in:
		@@ -1,5 +1,13 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-04-27 - 5.1.0 - feat(smartacme)
 | 
				
			||||||
 | 
					Implement exponential backoff retry logic and graceful shutdown handling in SmartAcme; update acme-client dependency to v5.4.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added retry helper with exponential backoff for ACME client operations
 | 
				
			||||||
 | 
					- Introduced retryOptions in ISmartAcmeOptions for configurable retry parameters
 | 
				
			||||||
 | 
					- Enhanced graceful shutdown handling by cleaning up pending DNS challenges on signal
 | 
				
			||||||
 | 
					- Updated acme-client dependency from v4.2.5 to v5.4.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 2025-04-26 - 5.0.1 - fix(build)
 | 
					## 2025-04-26 - 5.0.1 - fix(build)
 | 
				
			||||||
Update CI workflows, bump dependency versions, and refine import and TypeScript configuration
 | 
					Update CI workflows, bump dependency versions, and refine import and TypeScript configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,7 +51,7 @@
 | 
				
			|||||||
    "@push.rocks/smarttime": "^4.1.1",
 | 
					    "@push.rocks/smarttime": "^4.1.1",
 | 
				
			||||||
    "@push.rocks/smartunique": "^3.0.9",
 | 
					    "@push.rocks/smartunique": "^3.0.9",
 | 
				
			||||||
    "@tsclass/tsclass": "^9.0.0",
 | 
					    "@tsclass/tsclass": "^9.0.0",
 | 
				
			||||||
    "acme-client": "^4.2.5"
 | 
					    "acme-client": "^5.4.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@apiclient.xyz/cloudflare": "^6.3.2",
 | 
					    "@apiclient.xyz/cloudflare": "^6.3.2",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										68
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										68
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -45,8 +45,8 @@ importers:
 | 
				
			|||||||
        specifier: ^9.0.0
 | 
					        specifier: ^9.0.0
 | 
				
			||||||
        version: 9.0.0
 | 
					        version: 9.0.0
 | 
				
			||||||
      acme-client:
 | 
					      acme-client:
 | 
				
			||||||
        specifier: ^4.2.5
 | 
					        specifier: ^5.4.0
 | 
				
			||||||
        version: 4.2.5
 | 
					        version: 5.4.0
 | 
				
			||||||
    devDependencies:
 | 
					    devDependencies:
 | 
				
			||||||
      '@apiclient.xyz/cloudflare':
 | 
					      '@apiclient.xyz/cloudflare':
 | 
				
			||||||
        specifier: ^6.3.2
 | 
					        specifier: ^6.3.2
 | 
				
			||||||
@@ -1629,10 +1629,6 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
 | 
					    resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
 | 
				
			||||||
    engines: {node: '>= 0.6'}
 | 
					    engines: {node: '>= 0.6'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  acme-client@4.2.5:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-dtnck4sdZ2owFLTC73Ewjx0kmvsRjTRgaOc8UztCNODT+lr1DXj0tiuUXjeY4LAzZryXCtCib/E+KD8NYeP1aw==}
 | 
					 | 
				
			||||||
    engines: {node: '>= 10'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  acme-client@5.4.0:
 | 
					  acme-client@5.4.0:
 | 
				
			||||||
    resolution: {integrity: sha512-mORqg60S8iML6XSmVjqjGHJkINrCGLMj2QvDmFzI9vIlv1RGlyjmw3nrzaINJjkNsYXC41XhhD5pfy7CtuGcbA==}
 | 
					    resolution: {integrity: sha512-mORqg60S8iML6XSmVjqjGHJkINrCGLMj2QvDmFzI9vIlv1RGlyjmw3nrzaINJjkNsYXC41XhhD5pfy7CtuGcbA==}
 | 
				
			||||||
    engines: {node: '>= 16'}
 | 
					    engines: {node: '>= 16'}
 | 
				
			||||||
@@ -1719,18 +1715,12 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
 | 
					    resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
 | 
				
			||||||
    engines: {node: '>=4'}
 | 
					    engines: {node: '>=4'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  axios@0.26.1:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  axios@1.9.0:
 | 
					  axios@1.9.0:
 | 
				
			||||||
    resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
 | 
					    resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  b4a@1.6.7:
 | 
					  b4a@1.6.7:
 | 
				
			||||||
    resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
 | 
					    resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  backo2@1.0.2:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha1-MasayLEpNjRj41s+u2n038+6eUc=}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  bail@2.0.2:
 | 
					  bail@2.0.2:
 | 
				
			||||||
    resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
 | 
					    resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1778,9 +1768,6 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
 | 
					    resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
 | 
				
			||||||
    engines: {node: '>=10.0.0'}
 | 
					    engines: {node: '>=10.0.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bluebird@3.7.2:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  bn.js@4.12.2:
 | 
					  bn.js@4.12.2:
 | 
				
			||||||
    resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
 | 
					    resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2089,15 +2076,6 @@ packages:
 | 
				
			|||||||
      supports-color:
 | 
					      supports-color:
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  debug@4.3.4:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
 | 
					 | 
				
			||||||
    engines: {node: '>=6.0'}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      supports-color: '*'
 | 
					 | 
				
			||||||
    peerDependenciesMeta:
 | 
					 | 
				
			||||||
      supports-color:
 | 
					 | 
				
			||||||
        optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  debug@4.3.7:
 | 
					  debug@4.3.7:
 | 
				
			||||||
    resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
 | 
					    resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
 | 
				
			||||||
    engines: {node: '>=6.0'}
 | 
					    engines: {node: '>=6.0'}
 | 
				
			||||||
@@ -2460,15 +2438,6 @@ packages:
 | 
				
			|||||||
  fn.name@1.1.0:
 | 
					  fn.name@1.1.0:
 | 
				
			||||||
    resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
 | 
					    resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  follow-redirects@1.15.5:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==}
 | 
					 | 
				
			||||||
    engines: {node: '>=4.0'}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      debug: '*'
 | 
					 | 
				
			||||||
    peerDependenciesMeta:
 | 
					 | 
				
			||||||
      debug:
 | 
					 | 
				
			||||||
        optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  follow-redirects@1.15.9:
 | 
					  follow-redirects@1.15.9:
 | 
				
			||||||
    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
 | 
					    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
 | 
				
			||||||
    engines: {node: '>=4.0'}
 | 
					    engines: {node: '>=4.0'}
 | 
				
			||||||
@@ -3411,9 +3380,6 @@ packages:
 | 
				
			|||||||
  ms@2.0.0:
 | 
					  ms@2.0.0:
 | 
				
			||||||
    resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=}
 | 
					    resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ms@2.1.2:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ms@2.1.3:
 | 
					  ms@2.1.3:
 | 
				
			||||||
    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 | 
					    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7223,16 +7189,6 @@ snapshots:
 | 
				
			|||||||
      mime-types: 2.1.35
 | 
					      mime-types: 2.1.35
 | 
				
			||||||
      negotiator: 0.6.3
 | 
					      negotiator: 0.6.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  acme-client@4.2.5:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      axios: 0.26.1(debug@4.3.4)
 | 
					 | 
				
			||||||
      backo2: 1.0.2
 | 
					 | 
				
			||||||
      bluebird: 3.7.2
 | 
					 | 
				
			||||||
      debug: 4.3.4
 | 
					 | 
				
			||||||
      node-forge: 1.3.1
 | 
					 | 
				
			||||||
    transitivePeerDependencies:
 | 
					 | 
				
			||||||
      - supports-color
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  acme-client@5.4.0:
 | 
					  acme-client@5.4.0:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@peculiar/x509': 1.12.3
 | 
					      '@peculiar/x509': 1.12.3
 | 
				
			||||||
@@ -7308,12 +7264,6 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  axe-core@4.10.3: {}
 | 
					  axe-core@4.10.3: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  axios@0.26.1(debug@4.3.4):
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      follow-redirects: 1.15.5(debug@4.3.4)
 | 
					 | 
				
			||||||
    transitivePeerDependencies:
 | 
					 | 
				
			||||||
      - debug
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  axios@1.9.0(debug@4.4.0):
 | 
					  axios@1.9.0(debug@4.4.0):
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      follow-redirects: 1.15.9(debug@4.4.0)
 | 
					      follow-redirects: 1.15.9(debug@4.4.0)
 | 
				
			||||||
@@ -7324,8 +7274,6 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  b4a@1.6.7: {}
 | 
					  b4a@1.6.7: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  backo2@1.0.2: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  bail@2.0.2: {}
 | 
					  bail@2.0.2: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  balanced-match@1.0.2: {}
 | 
					  balanced-match@1.0.2: {}
 | 
				
			||||||
@@ -7361,8 +7309,6 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  basic-ftp@5.0.5: {}
 | 
					  basic-ftp@5.0.5: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bluebird@3.7.2: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  bn.js@4.12.2: {}
 | 
					  bn.js@4.12.2: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  body-parser@1.20.3:
 | 
					  body-parser@1.20.3:
 | 
				
			||||||
@@ -7681,10 +7627,6 @@ snapshots:
 | 
				
			|||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      ms: 2.1.3
 | 
					      ms: 2.1.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  debug@4.3.4:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      ms: 2.1.2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  debug@4.3.7:
 | 
					  debug@4.3.7:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      ms: 2.1.3
 | 
					      ms: 2.1.3
 | 
				
			||||||
@@ -8125,10 +8067,6 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  fn.name@1.1.0: {}
 | 
					  fn.name@1.1.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  follow-redirects@1.15.5(debug@4.3.4):
 | 
					 | 
				
			||||||
    optionalDependencies:
 | 
					 | 
				
			||||||
      debug: 4.3.4
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  follow-redirects@1.15.9(debug@4.4.0):
 | 
					  follow-redirects@1.15.9(debug@4.4.0):
 | 
				
			||||||
    optionalDependencies:
 | 
					    optionalDependencies:
 | 
				
			||||||
      debug: 4.4.0
 | 
					      debug: 4.4.0
 | 
				
			||||||
@@ -9337,8 +9275,6 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ms@2.0.0: {}
 | 
					  ms@2.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ms@2.1.2: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ms@2.1.3: {}
 | 
					  ms@2.1.3: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  nanocolors@0.2.13: {}
 | 
					  nanocolors@0.2.13: {}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,6 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const commitinfo = {
 | 
					export const commitinfo = {
 | 
				
			||||||
  name: '@push.rocks/smartacme',
 | 
					  name: '@push.rocks/smartacme',
 | 
				
			||||||
  version: '5.0.1',
 | 
					  version: '5.1.0',
 | 
				
			||||||
  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.'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,19 @@ export interface ISmartAcmeOptions {
 | 
				
			|||||||
  setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
 | 
					  setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
 | 
				
			||||||
  removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
 | 
					  removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
 | 
				
			||||||
  environment: 'production' | 'integration';
 | 
					  environment: 'production' | 'integration';
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Optional retry/backoff configuration for transient failures
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  retryOptions?: {
 | 
				
			||||||
 | 
					    /** number of retry attempts */
 | 
				
			||||||
 | 
					    retries?: number;
 | 
				
			||||||
 | 
					    /** backoff multiplier */
 | 
				
			||||||
 | 
					    factor?: number;
 | 
				
			||||||
 | 
					    /** initial delay in milliseconds */
 | 
				
			||||||
 | 
					    minTimeoutMs?: number;
 | 
				
			||||||
 | 
					    /** maximum delay cap in milliseconds */
 | 
				
			||||||
 | 
					    maxTimeoutMs?: number;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -30,7 +43,7 @@ export class SmartAcme {
 | 
				
			|||||||
  private options: ISmartAcmeOptions;
 | 
					  private options: ISmartAcmeOptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // the acme client
 | 
					  // the acme client
 | 
				
			||||||
  private client: any;
 | 
					  private client: plugins.acme.Client;
 | 
				
			||||||
  private smartdns = new plugins.smartdnsClient.Smartdns({});
 | 
					  private smartdns = new plugins.smartdnsClient.Smartdns({});
 | 
				
			||||||
  public logger: plugins.smartlog.Smartlog;
 | 
					  public logger: plugins.smartlog.Smartlog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -44,10 +57,23 @@ export class SmartAcme {
 | 
				
			|||||||
  // certmanager
 | 
					  // certmanager
 | 
				
			||||||
  private certmanager: SmartacmeCertManager;
 | 
					  private certmanager: SmartacmeCertManager;
 | 
				
			||||||
  private certmatcher: SmartacmeCertMatcher;
 | 
					  private certmatcher: SmartacmeCertMatcher;
 | 
				
			||||||
 | 
					  // retry/backoff configuration (resolved with defaults)
 | 
				
			||||||
 | 
					  private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
 | 
				
			||||||
 | 
					  // track pending DNS challenges for graceful shutdown
 | 
				
			||||||
 | 
					  private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(optionsArg: ISmartAcmeOptions) {
 | 
					  constructor(optionsArg: ISmartAcmeOptions) {
 | 
				
			||||||
    this.options = optionsArg;
 | 
					    this.options = optionsArg;
 | 
				
			||||||
    this.logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
 | 
					    this.logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
 | 
				
			||||||
 | 
					    // enable console output for structured logging
 | 
				
			||||||
 | 
					    this.logger.enableConsole();
 | 
				
			||||||
 | 
					    // initialize retry/backoff options
 | 
				
			||||||
 | 
					    this.retryOptions = {
 | 
				
			||||||
 | 
					      retries: optionsArg.retryOptions?.retries ?? 3,
 | 
				
			||||||
 | 
					      factor: optionsArg.retryOptions?.factor ?? 2,
 | 
				
			||||||
 | 
					      minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
 | 
				
			||||||
 | 
					      maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -88,11 +114,55 @@ export class SmartAcme {
 | 
				
			|||||||
      termsOfServiceAgreed: true,
 | 
					      termsOfServiceAgreed: true,
 | 
				
			||||||
      contact: [`mailto:${this.options.accountEmail}`],
 | 
					      contact: [`mailto:${this.options.accountEmail}`],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    // Setup graceful shutdown handlers
 | 
				
			||||||
 | 
					    process.on('SIGINT', () => this.handleSignal('SIGINT'));
 | 
				
			||||||
 | 
					    process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async stop() {
 | 
					    public async stop() {
 | 
				
			||||||
    await this.certmanager.smartdataDb.close();
 | 
					      await this.certmanager.smartdataDb.close();
 | 
				
			||||||
  }
 | 
					    }
 | 
				
			||||||
 | 
					    /** Retry helper with exponential backoff */
 | 
				
			||||||
 | 
					    private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
 | 
				
			||||||
 | 
					      let attempt = 0;
 | 
				
			||||||
 | 
					      let delay = this.retryOptions.minTimeoutMs;
 | 
				
			||||||
 | 
					      while (true) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          return await operation();
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					          attempt++;
 | 
				
			||||||
 | 
					          if (attempt > this.retryOptions.retries) {
 | 
				
			||||||
 | 
					            await this.logger.log('error', `Operation ${operationName} failed after ${attempt} attempts`, err);
 | 
				
			||||||
 | 
					            throw err;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          await this.logger.log('warn', `Operation ${operationName} failed on attempt ${attempt}, retrying in ${delay}ms`, err);
 | 
				
			||||||
 | 
					          await plugins.smartdelay.delayFor(delay);
 | 
				
			||||||
 | 
					          delay = Math.min(delay * this.retryOptions.factor, this.retryOptions.maxTimeoutMs);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /** Clean up pending challenges and shut down */
 | 
				
			||||||
 | 
					    private async handleShutdown(): Promise<void> {
 | 
				
			||||||
 | 
					      for (const challenge of [...this.pendingChallenges]) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          await this.removeChallenge(challenge);
 | 
				
			||||||
 | 
					          await this.logger.log('info', 'Removed pending challenge during shutdown', challenge);
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					          await this.logger.log('error', 'Failed to remove pending challenge during shutdown', err);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.pendingChallenges = [];
 | 
				
			||||||
 | 
					      await this.stop();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /** Handle process signals for graceful shutdown */
 | 
				
			||||||
 | 
					    private handleSignal(sig: string): void {
 | 
				
			||||||
 | 
					      this.logger.log('info', `Received signal ${sig}, shutting down gracefully`);
 | 
				
			||||||
 | 
					      this.handleShutdown()
 | 
				
			||||||
 | 
					        .then(() => process.exit(0))
 | 
				
			||||||
 | 
					        .catch((err) => {
 | 
				
			||||||
 | 
					          this.logger.log('error', 'Error during shutdown', err).then(() => process.exit(1));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * gets a certificate
 | 
					   * gets a certificate
 | 
				
			||||||
@@ -128,54 +198,54 @@ 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.certmanager.interestMap.addInterest(certDomainName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Place new order */
 | 
					    /* Place new order with retry */
 | 
				
			||||||
    const order = await this.client.createOrder({
 | 
					    const order = await this.retry(() => this.client.createOrder({
 | 
				
			||||||
      identifiers: [
 | 
					      identifiers: [
 | 
				
			||||||
        { type: 'dns', value: certDomainName },
 | 
					        { type: 'dns', value: certDomainName },
 | 
				
			||||||
        { type: 'dns', value: `*.${certDomainName}` },
 | 
					        { type: 'dns', value: `*.${certDomainName}` },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    });
 | 
					    }), 'createOrder');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Get authorizations and select challenges */
 | 
					    /* Get authorizations and select challenges */
 | 
				
			||||||
    const authorizations = await this.client.getAuthorizations(order);
 | 
					    const authorizations = await this.retry(() => this.client.getAuthorizations(order), 'getAuthorizations');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const authz of authorizations) {
 | 
					    for (const authz of authorizations) {
 | 
				
			||||||
      console.log(authz);
 | 
					      await this.logger.log('debug', 'Authorization received', authz);
 | 
				
			||||||
      const fullHostName: string = `_acme-challenge.${authz.identifier.value}`;
 | 
					      const fullHostName: string = `_acme-challenge.${authz.identifier.value}`;
 | 
				
			||||||
      const dnsChallenge: string = authz.challenges.find((challengeArg) => {
 | 
					      const dnsChallenge = authz.challenges.find((challengeArg) => {
 | 
				
			||||||
        return challengeArg.type === 'dns-01';
 | 
					        return challengeArg.type === 'dns-01';
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      // process.exit(1);
 | 
					      // process.exit(1);
 | 
				
			||||||
      const keyAuthorization: string = await this.client.getChallengeKeyAuthorization(dnsChallenge);
 | 
					      const keyAuthorization: string = await this.client.getChallengeKeyAuthorization(dnsChallenge);
 | 
				
			||||||
 | 
					      // prepare DNS challenge record and track for cleanup
 | 
				
			||||||
 | 
					      const challengeRecord: plugins.tsclass.network.IDnsChallenge = { hostName: fullHostName, challenge: keyAuthorization };
 | 
				
			||||||
 | 
					      this.pendingChallenges.push(challengeRecord);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        /* Satisfy challenge */
 | 
					        /* Satisfy challenge */
 | 
				
			||||||
        await this.setChallenge({
 | 
					        await this.retry(() => this.setChallenge(challengeRecord), 'setChallenge');
 | 
				
			||||||
          hostName: fullHostName,
 | 
					 | 
				
			||||||
          challenge: keyAuthorization,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        await plugins.smartdelay.delayFor(30000);
 | 
					        await plugins.smartdelay.delayFor(30000);
 | 
				
			||||||
        await this.smartdns.checkUntilAvailable(fullHostName, 'TXT', keyAuthorization, 100, 5000);
 | 
					        await this.retry(() => this.smartdns.checkUntilAvailable(fullHostName, 'TXT', keyAuthorization, 100, 5000), 'dnsCheckUntilAvailable');
 | 
				
			||||||
        console.log('Cool down an extra 60 second for region availability');
 | 
					        await this.logger.log('info', 'Cooling down extra 60 seconds for DNS regional propagation');
 | 
				
			||||||
        await plugins.smartdelay.delayFor(60000);
 | 
					        await plugins.smartdelay.delayFor(60000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /* Verify that challenge is satisfied */
 | 
					        /* Verify that challenge is satisfied */
 | 
				
			||||||
        await this.client.verifyChallenge(authz, dnsChallenge);
 | 
					        await this.retry(() => this.client.verifyChallenge(authz, dnsChallenge), 'verifyChallenge');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /* Notify ACME provider that challenge is satisfied */
 | 
					        /* Notify ACME provider that challenge is satisfied */
 | 
				
			||||||
        await this.client.completeChallenge(dnsChallenge);
 | 
					        await this.retry(() => this.client.completeChallenge(dnsChallenge), 'completeChallenge');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /* Wait for ACME provider to respond with valid status */
 | 
					        /* Wait for ACME provider to respond with valid status */
 | 
				
			||||||
        await this.client.waitForValidStatus(dnsChallenge);
 | 
					        await this.retry(() => this.client.waitForValidStatus(dnsChallenge), 'waitForValidStatus');
 | 
				
			||||||
      } finally {
 | 
					      } finally {
 | 
				
			||||||
        /* Clean up challenge response */
 | 
					        /* Clean up challenge response */
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          await this.removeChallenge({
 | 
					          await this.retry(() => this.removeChallenge(challengeRecord), 'removeChallenge');
 | 
				
			||||||
            hostName: fullHostName,
 | 
					        } catch (err) {
 | 
				
			||||||
            challenge: keyAuthorization,
 | 
					          await this.logger.log('error', 'Error removing DNS challenge', err);
 | 
				
			||||||
          });
 | 
					        } finally {
 | 
				
			||||||
        } catch (e) {
 | 
					          // remove from pending list
 | 
				
			||||||
          console.log(e);
 | 
					          this.pendingChallenges = this.pendingChallenges.filter(c => c !== challengeRecord);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -186,8 +256,8 @@ export class SmartAcme {
 | 
				
			|||||||
      altNames: [certDomainName],
 | 
					      altNames: [certDomainName],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.client.finalizeOrder(order, csr);
 | 
					    await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
 | 
				
			||||||
    const cert = await this.client.getCertificate(order);
 | 
					    const cert = await this.retry(() => this.client.getCertificate(order), 'getCertificate');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Done */
 | 
					    /* Done */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user