Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cf3dbdd95 | |||
| 1c75bac44f | |||
| a865fb89e0 | |||
| cfc0695c8a |
18
changelog.md
18
changelog.md
@@ -1,5 +1,23 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-15 - 9.1.1 - fix(deps)
|
||||||
|
bump @push.rocks/smarttime to ^4.2.3 and @push.rocks/taskbuffer to ^6.1.2
|
||||||
|
|
||||||
|
- @push.rocks/smarttime: ^4.1.1 -> ^4.2.3
|
||||||
|
- @push.rocks/taskbuffer: ^6.1.0 -> ^6.1.2
|
||||||
|
- Only package.json dependency version updates; no code changes
|
||||||
|
|
||||||
|
## 2026-02-15 - 9.1.0 - feat(smartacme)
|
||||||
|
Integrate @push.rocks/taskbuffer TaskManager to coordinate ACME certificate issuance with per-domain mutex, global concurrency cap, and account-level rate limiting; refactor issuance flow into a single reusable cert-issuance task, expose issuance events, and update lifecycle to start/stop the TaskManager. Add configuration for concurrent issuances and sliding-window order limits, export taskbuffer types/plugins, and update tests and docs accordingly.
|
||||||
|
|
||||||
|
- Added dependency @push.rocks/taskbuffer and re-exported ITaskEvent/ITaskMetadata in ts/index.ts; also imported/exported taskbuffer in ts/plugins.ts.
|
||||||
|
- Replaced interestMap coordination with TaskManager + TaskConstraintGroup(s): 'cert-domain-mutex' (per-domain mutex, resultSharingMode: 'share-latest'), 'acme-global-concurrency' (global concurrency cap), and 'acme-account-rate-limit' (sliding-window rate limiter).
|
||||||
|
- Introduced a single reusable Task named 'cert-issuance' and moved the ACME issuance flow into performCertificateIssuance(), splitting progress into named steps (prepare/authorize/finalize/store) and using notifyStep() for observable progress.
|
||||||
|
- Exposed certIssuanceEvents via SmartAcme.certIssuanceEvents and wired TaskManager.start()/stop() into SmartAcme.start()/stop().
|
||||||
|
- Added new ISmartAcmeOptions: maxConcurrentIssuances, maxOrdersPerWindow, orderWindowMs to control concurrency and rate limiting.
|
||||||
|
- Updated tests to remove interestMap stubs and adapt to the taskbuffer-based flow; cleaned up client/retry stubbing in tests.
|
||||||
|
- Updated readme.hints.md with guidance on concurrency, rate limiting, and taskbuffer integration.
|
||||||
|
|
||||||
## 2026-02-15 - 9.0.1 - fix(acme-http-client)
|
## 2026-02-15 - 9.0.1 - fix(acme-http-client)
|
||||||
Destroy keep-alive HTTP agents and DNS client on shutdown to allow process exit; add destroy() on AcmeHttpClient and AcmeClient, wire agents into requests, and call client/smartdns destroy during SmartAcme.stop; documentation clarifications and expanded README (error handling, examples, default retry values).
|
Destroy keep-alive HTTP agents and DNS client on shutdown to allow process exit; add destroy() on AcmeHttpClient and AcmeClient, wire agents into requests, and call client/smartdns destroy during SmartAcme.stop; documentation clarifications and expanded README (error handling, examples, default retry values).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartacme",
|
"name": "@push.rocks/smartacme",
|
||||||
"version": "9.0.1",
|
"version": "9.1.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -48,8 +48,9 @@
|
|||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartlog": "^3.1.10",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartstring": "^4.1.0",
|
"@push.rocks/smartstring": "^4.1.0",
|
||||||
"@push.rocks/smarttime": "^4.1.1",
|
"@push.rocks/smarttime": "^4.2.3",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
|
"@push.rocks/taskbuffer": "^6.1.2",
|
||||||
"@tsclass/tsclass": "^9.3.0"
|
"@tsclass/tsclass": "^9.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
125
pnpm-lock.yaml
generated
125
pnpm-lock.yaml
generated
@@ -36,11 +36,14 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
'@push.rocks/smarttime':
|
'@push.rocks/smarttime':
|
||||||
specifier: ^4.1.1
|
specifier: ^4.2.3
|
||||||
version: 4.1.1
|
version: 4.2.3
|
||||||
'@push.rocks/smartunique':
|
'@push.rocks/smartunique':
|
||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
|
'@push.rocks/taskbuffer':
|
||||||
|
specifier: ^6.1.2
|
||||||
|
version: 6.1.2
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.3.0
|
specifier: ^9.3.0
|
||||||
version: 9.3.0
|
version: 9.3.0
|
||||||
@@ -1012,8 +1015,8 @@ packages:
|
|||||||
'@push.rocks/smartstring@4.1.0':
|
'@push.rocks/smartstring@4.1.0':
|
||||||
resolution: {integrity: sha512-Q4py/Nm3KTDhQ9EiC75yBtSTLR0KLMwhKM+8gGcutgKotZT6wJ3gncjmtD8LKFfNhb4lSaFMgPJgLrCHTOH6Iw==}
|
resolution: {integrity: sha512-Q4py/Nm3KTDhQ9EiC75yBtSTLR0KLMwhKM+8gGcutgKotZT6wJ3gncjmtD8LKFfNhb4lSaFMgPJgLrCHTOH6Iw==}
|
||||||
|
|
||||||
'@push.rocks/smarttime@4.1.1':
|
'@push.rocks/smarttime@4.2.3':
|
||||||
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
|
resolution: {integrity: sha512-8gMg8RUkrCG4p9NcEUZV7V6KpL24+jAMK02g7qyhfA6giz/JJWD0+8w8xjSR+G7qe16KVQ2y3RbvAL9TxmO36g==}
|
||||||
|
|
||||||
'@push.rocks/smartunique@3.0.9':
|
'@push.rocks/smartunique@3.0.9':
|
||||||
resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==}
|
resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==}
|
||||||
@@ -1033,12 +1036,12 @@ packages:
|
|||||||
'@push.rocks/smartyaml@3.0.4':
|
'@push.rocks/smartyaml@3.0.4':
|
||||||
resolution: {integrity: sha512-1JRt+hnoc2zHw3AW+vXKlCdSVwqOmY/01fu+2HBviS0UDjoZCa+/rp6E3GaQb5lEEafKi8ENbffAfjXXp3N2xQ==}
|
resolution: {integrity: sha512-1JRt+hnoc2zHw3AW+vXKlCdSVwqOmY/01fu+2HBviS0UDjoZCa+/rp6E3GaQb5lEEafKi8ENbffAfjXXp3N2xQ==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
|
||||||
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.5.0':
|
'@push.rocks/taskbuffer@3.5.0':
|
||||||
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@6.1.2':
|
||||||
|
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
|
||||||
|
|
||||||
'@push.rocks/webrequest@3.0.37':
|
'@push.rocks/webrequest@3.0.37':
|
||||||
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
||||||
|
|
||||||
@@ -2290,6 +2293,10 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
croner@10.0.1:
|
||||||
|
resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==}
|
||||||
|
engines: {node: '>=18.0'}
|
||||||
|
|
||||||
croner@4.4.1:
|
croner@4.4.1:
|
||||||
resolution: {integrity: sha512-aqVeeIPCf5/NZFlz4mN4MLEOs9xf4ODCmHQDs+577JFj8mK3RkKJz77h7+Rn94AijUqKdFNOUHM+v88d8p02UQ==}
|
resolution: {integrity: sha512-aqVeeIPCf5/NZFlz4mN4MLEOs9xf4ODCmHQDs+577JFj8mK3RkKJz77h7+Rn94AijUqKdFNOUHM+v88d8p02UQ==}
|
||||||
|
|
||||||
@@ -2297,10 +2304,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9pSLe+tDJnmNak2JeMkz6ZmTCXP5p6vCxSd4kvDqrTJkqAP62j2uAEIZjf8cPDZIakStujqVzh5Y5MIWH3yYAw==}
|
resolution: {integrity: sha512-9pSLe+tDJnmNak2JeMkz6ZmTCXP5p6vCxSd4kvDqrTJkqAP62j2uAEIZjf8cPDZIakStujqVzh5Y5MIWH3yYAw==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|
||||||
croner@9.0.0:
|
|
||||||
resolution: {integrity: sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==}
|
|
||||||
engines: {node: '>=18.0'}
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -2319,6 +2322,9 @@ packages:
|
|||||||
dayjs@1.11.13:
|
dayjs@1.11.13:
|
||||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||||
|
|
||||||
|
dayjs@1.11.19:
|
||||||
|
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3584,8 +3590,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==}
|
resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
pretty-ms@9.2.0:
|
pretty-ms@9.3.0:
|
||||||
resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
|
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
progress@2.0.3:
|
progress@2.0.3:
|
||||||
@@ -4292,7 +4298,7 @@ snapshots:
|
|||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartsitemap': 2.0.4
|
'@push.rocks/smartsitemap': 2.0.4
|
||||||
'@push.rocks/smartstream': 3.2.5
|
'@push.rocks/smartstream': 3.2.5
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/taskbuffer': 3.5.0
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@push.rocks/webrequest': 3.0.37
|
'@push.rocks/webrequest': 3.0.37
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
@@ -5533,8 +5539,11 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
- react
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.8.3':
|
'@git.zone/tsbundle@2.8.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5556,8 +5565,11 @@ snapshots:
|
|||||||
rolldown: 1.0.0-beta.52
|
rolldown: 1.0.0-beta.52
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
|
- react
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@git.zone/tspublish@1.11.0':
|
'@git.zone/tspublish@1.11.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5573,8 +5585,11 @@ snapshots:
|
|||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
- react
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@git.zone/tsrun@2.0.1':
|
'@git.zone/tsrun@2.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5605,7 +5620,7 @@ snapshots:
|
|||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smarts3': 3.0.3
|
'@push.rocks/smarts3': 3.0.3
|
||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
figures: 6.1.0
|
figures: 6.1.0
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
@@ -5945,10 +5960,14 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 4.4.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/lik@6.2.2':
|
'@push.rocks/lik@6.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5956,7 +5975,7 @@ snapshots:
|
|||||||
'@push.rocks/smartmatch': 2.0.0
|
'@push.rocks/smartmatch': 2.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/symbol-tree': 3.2.5
|
'@types/symbol-tree': 3.2.5
|
||||||
symbol-tree: 3.2.4
|
symbol-tree: 3.2.4
|
||||||
@@ -5982,8 +6001,13 @@ snapshots:
|
|||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/qenv@6.1.3':
|
'@push.rocks/qenv@6.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6106,21 +6130,24 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@tsclass/tsclass': 8.2.1
|
'@tsclass/tsclass': 8.2.1
|
||||||
mongodb: 6.16.0
|
mongodb: 6.16.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- gcp-metadata
|
- gcp-metadata
|
||||||
- kerberos
|
- kerberos
|
||||||
- mongodb-client-encryption
|
- mongodb-client-encryption
|
||||||
|
- react
|
||||||
- snappy
|
- snappy
|
||||||
- socks
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartdata@7.0.15':
|
'@push.rocks/smartdata@7.0.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6131,7 +6158,7 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.5.0
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
@@ -6302,7 +6329,7 @@ snapshots:
|
|||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smarthash': 3.2.6
|
'@push.rocks/smarthash': 3.2.6
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/webrequest': 4.0.2
|
'@push.rocks/webrequest': 4.0.2
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
|
|
||||||
@@ -6347,13 +6374,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- gcp-metadata
|
- gcp-metadata
|
||||||
- kerberos
|
- kerberos
|
||||||
- mongodb-client-encryption
|
- mongodb-client-encryption
|
||||||
|
- react
|
||||||
- snappy
|
- snappy
|
||||||
- socks
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartnetwork@3.0.2':
|
'@push.rocks/smartnetwork@3.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6385,12 +6415,15 @@ snapshots:
|
|||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 4.4.2
|
'@push.rocks/smartrequest': 4.4.2
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartversion': 3.0.5
|
'@push.rocks/smartversion': 3.0.5
|
||||||
package-json: 8.1.1
|
package-json: 8.1.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
- react
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartntml@2.0.8':
|
'@push.rocks/smartntml@2.0.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6555,7 +6588,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
engine.io: 6.6.4
|
engine.io: 6.6.4
|
||||||
socket.io: 4.8.1
|
socket.io: 4.8.1
|
||||||
socket.io-client: 4.8.1
|
socket.io-client: 4.8.1
|
||||||
@@ -6605,16 +6638,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/isounique': 1.0.5
|
'@push.rocks/isounique': 1.0.5
|
||||||
|
|
||||||
'@push.rocks/smarttime@4.1.1':
|
'@push.rocks/smarttime@4.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
croner: 9.0.0
|
croner: 10.0.1
|
||||||
date-fns: 4.1.0
|
date-fns: 4.1.0
|
||||||
dayjs: 1.11.13
|
dayjs: 1.11.19
|
||||||
is-nan: 1.3.2
|
is-nan: 1.3.2
|
||||||
pretty-ms: 9.2.0
|
pretty-ms: 9.3.0
|
||||||
|
|
||||||
'@push.rocks/smartunique@3.0.9':
|
'@push.rocks/smartunique@3.0.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6642,16 +6675,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yaml: 2.8.2
|
yaml: 2.8.2
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
|
||||||
dependencies:
|
|
||||||
'@push.rocks/lik': 6.2.2
|
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
|
||||||
'@push.rocks/smartlog': 3.1.11
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrx': 3.0.10
|
|
||||||
'@push.rocks/smarttime': 4.1.1
|
|
||||||
'@push.rocks/smartunique': 3.0.9
|
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.5.0':
|
'@push.rocks/taskbuffer@3.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
@@ -6660,7 +6683,23 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
|
'@push.rocks/smartunique': 3.0.9
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@6.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@design.estate/dees-element': 2.1.6
|
||||||
|
'@push.rocks/lik': 6.2.2
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartlog': 3.1.11
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
@@ -8224,12 +8263,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
croner@10.0.1: {}
|
||||||
|
|
||||||
croner@4.4.1: {}
|
croner@4.4.1: {}
|
||||||
|
|
||||||
croner@5.7.0: {}
|
croner@5.7.0: {}
|
||||||
|
|
||||||
croner@9.0.0: {}
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -8246,6 +8285,8 @@ snapshots:
|
|||||||
|
|
||||||
dayjs@1.11.13: {}
|
dayjs@1.11.13: {}
|
||||||
|
|
||||||
|
dayjs@1.11.19: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.0.0
|
ms: 2.0.0
|
||||||
@@ -9729,7 +9770,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
parse-ms: 3.0.0
|
parse-ms: 3.0.0
|
||||||
|
|
||||||
pretty-ms@9.2.0:
|
pretty-ms@9.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
parse-ms: 4.0.0
|
parse-ms: 4.0.0
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ Key files:
|
|||||||
|
|
||||||
Usage in `ts/plugins.ts`: `import * as acme from './acme/index.js'` (replaces `acme-client`)
|
Usage in `ts/plugins.ts`: `import * as acme from './acme/index.js'` (replaces `acme-client`)
|
||||||
|
|
||||||
|
## Concurrency & Rate Limiting (taskbuffer integration)
|
||||||
|
|
||||||
|
As of v9.1.0, `@push.rocks/lik.InterestMap` was replaced with `@push.rocks/taskbuffer.TaskManager` for coordinating concurrent certificate requests. This provides:
|
||||||
|
|
||||||
|
- **Per-domain mutex** (`cert-domain-mutex`): Only one ACME issuance per TLD at a time, with `resultSharingMode: 'share-latest'` so queued callers get the same result without re-issuing.
|
||||||
|
- **Global concurrency cap** (`acme-global-concurrency`): Limits total parallel ACME operations (default 5, configurable via `maxConcurrentIssuances`).
|
||||||
|
- **Account-level rate limiting** (`acme-account-rate-limit`): Sliding-window rate limit (default 250 orders per 3 hours, configurable via `maxOrdersPerWindow`/`orderWindowMs`) to stay under Let's Encrypt limits.
|
||||||
|
- **Step-based progress**: The cert issuance task uses `notifyStep()` for prepare/authorize/finalize/store phases, observable via `smartAcme.certIssuanceEvents`.
|
||||||
|
|
||||||
|
Key implementation details:
|
||||||
|
- A single reusable `Task` named `cert-issuance` handles all domains via `triggerTaskConstrained()` with different inputs.
|
||||||
|
- The `shouldExecute` callback on the domain mutex checks the certmanager cache as a safety net.
|
||||||
|
- `TaskManager.start()` is called in `SmartAcme.start()` and `TaskManager.stop()` in `SmartAcme.stop()`.
|
||||||
|
- The "no cronjobs specified" log messages during tests come from taskbuffer's internal CronManager polling — harmless noise when no cron tasks are scheduled.
|
||||||
|
|
||||||
## Dependency Notes
|
## Dependency Notes
|
||||||
|
|
||||||
- `acme-client` was replaced with custom implementation in `ts/acme/` + `@peculiar/x509` for CSR generation
|
- `acme-client` was replaced with custom implementation in `ts/acme/` + `@peculiar/x509` for CSR generation
|
||||||
|
|||||||
@@ -25,16 +25,12 @@ tap.test('HTTP-01 only configuration should work for regular domains', async ()
|
|||||||
smartAcmeInstance.certmatcher = {
|
smartAcmeInstance.certmatcher = {
|
||||||
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
|
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
|
||||||
} as any;
|
} as any;
|
||||||
smartAcmeInstance.interestMap = {
|
|
||||||
checkInterest: async () => false,
|
|
||||||
addInterest: async () => ({ interestFullfilled: new Promise(() => {}), fullfillInterest: () => {}, destroy: () => {} } as any)
|
|
||||||
} as any;
|
|
||||||
await smartAcmeInstance.certmanager.init();
|
await smartAcmeInstance.certmanager.init();
|
||||||
};
|
};
|
||||||
await smartAcmeInstance.start();
|
await smartAcmeInstance.start();
|
||||||
|
|
||||||
// Stub the core certificate methods to avoid actual ACME calls
|
// Stub the core certificate methods to avoid actual ACME calls
|
||||||
smartAcmeInstance.client = {
|
(smartAcmeInstance as any).client = {
|
||||||
createOrder: async (orderPayload: any) => {
|
createOrder: async (orderPayload: any) => {
|
||||||
// Verify no wildcard is included in default request
|
// Verify no wildcard is included in default request
|
||||||
const identifiers = orderPayload.identifiers;
|
const identifiers = orderPayload.identifiers;
|
||||||
@@ -48,7 +44,7 @@ tap.test('HTTP-01 only configuration should work for regular domains', async ()
|
|||||||
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
|
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
|
(smartAcmeInstance as any).retry = async (fn: () => Promise<any>) => fn();
|
||||||
|
|
||||||
// Mock certmanager methods
|
// Mock certmanager methods
|
||||||
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
|
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
|
||||||
@@ -83,16 +79,12 @@ tap.test('should only include wildcard when explicitly requested with DNS-01', a
|
|||||||
smartAcmeInstance.certmatcher = {
|
smartAcmeInstance.certmatcher = {
|
||||||
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
|
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
|
||||||
} as any;
|
} as any;
|
||||||
smartAcmeInstance.interestMap = {
|
|
||||||
checkInterest: async () => false,
|
|
||||||
addInterest: async () => ({ interestFullfilled: new Promise(() => {}), fullfillInterest: () => {}, destroy: () => {} } as any)
|
|
||||||
} as any;
|
|
||||||
await smartAcmeInstance.certmanager.init();
|
await smartAcmeInstance.certmanager.init();
|
||||||
};
|
};
|
||||||
await smartAcmeInstance.start();
|
await smartAcmeInstance.start();
|
||||||
|
|
||||||
// Stub the core certificate methods
|
// Stub the core certificate methods
|
||||||
smartAcmeInstance.client = {
|
(smartAcmeInstance as any).client = {
|
||||||
createOrder: async (orderPayload: any) => {
|
createOrder: async (orderPayload: any) => {
|
||||||
const identifiers = orderPayload.identifiers;
|
const identifiers = orderPayload.identifiers;
|
||||||
expect(identifiers.length).toEqual(2);
|
expect(identifiers.length).toEqual(2);
|
||||||
@@ -105,7 +97,7 @@ tap.test('should only include wildcard when explicitly requested with DNS-01', a
|
|||||||
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
|
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
|
(smartAcmeInstance as any).retry = async (fn: () => Promise<any>) => fn();
|
||||||
|
|
||||||
// Mock certmanager methods
|
// Mock certmanager methods
|
||||||
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
|
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
|
||||||
@@ -136,10 +128,6 @@ tap.test('should skip wildcard if requested but no DNS-01 handler available', as
|
|||||||
smartAcmeInstance.certmatcher = {
|
smartAcmeInstance.certmatcher = {
|
||||||
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
|
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
|
||||||
} as any;
|
} as any;
|
||||||
smartAcmeInstance.interestMap = {
|
|
||||||
checkInterest: async () => false,
|
|
||||||
addInterest: async () => ({ interestFullfilled: new Promise(() => {}), fullfillInterest: () => {}, destroy: () => {} } as any)
|
|
||||||
} as any;
|
|
||||||
await smartAcmeInstance.certmanager.init();
|
await smartAcmeInstance.certmanager.init();
|
||||||
};
|
};
|
||||||
await smartAcmeInstance.start();
|
await smartAcmeInstance.start();
|
||||||
@@ -154,7 +142,7 @@ tap.test('should skip wildcard if requested but no DNS-01 handler available', as
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Stub the core certificate methods
|
// Stub the core certificate methods
|
||||||
smartAcmeInstance.client = {
|
(smartAcmeInstance as any).client = {
|
||||||
createOrder: async (orderPayload: any) => {
|
createOrder: async (orderPayload: any) => {
|
||||||
const identifiers = orderPayload.identifiers;
|
const identifiers = orderPayload.identifiers;
|
||||||
// Should only have regular domain, no wildcard
|
// Should only have regular domain, no wildcard
|
||||||
@@ -167,7 +155,7 @@ tap.test('should skip wildcard if requested but no DNS-01 handler available', as
|
|||||||
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
|
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
|
(smartAcmeInstance as any).retry = async (fn: () => Promise<any>) => fn();
|
||||||
|
|
||||||
// Mock certmanager methods
|
// Mock certmanager methods
|
||||||
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
|
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartacme',
|
name: '@push.rocks/smartacme',
|
||||||
version: '9.0.1',
|
version: '9.1.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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ export { certmanagers };
|
|||||||
// handlers
|
// handlers
|
||||||
import * as handlers from './handlers/index.js';
|
import * as handlers from './handlers/index.js';
|
||||||
export { handlers };
|
export { handlers };
|
||||||
|
|
||||||
|
// re-export taskbuffer event types for consumers
|
||||||
|
export type { ITaskEvent, ITaskMetadata } from '@push.rocks/taskbuffer';
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import * as smartnetwork from '@push.rocks/smartnetwork';
|
|||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
import * as smartstring from '@push.rocks/smartstring';
|
import * as smartstring from '@push.rocks/smartstring';
|
||||||
import * as smarttime from '@push.rocks/smarttime';
|
import * as smarttime from '@push.rocks/smarttime';
|
||||||
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
lik,
|
lik,
|
||||||
@@ -30,6 +31,7 @@ export {
|
|||||||
smartunique,
|
smartunique,
|
||||||
smartstring,
|
smartstring,
|
||||||
smarttime,
|
smarttime,
|
||||||
|
taskbuffer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// @tsclass scope
|
// @tsclass scope
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
|
|||||||
import { commitinfo } from './00_commitinfo_data.js';
|
import { commitinfo } from './00_commitinfo_data.js';
|
||||||
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
||||||
|
|
||||||
|
// ── Types & constants for certificate issuance task ──────────────────────────
|
||||||
|
|
||||||
|
interface ICertIssuanceInput {
|
||||||
|
certDomainName: string;
|
||||||
|
domainArg: string;
|
||||||
|
isWildcardRequest: boolean;
|
||||||
|
includeWildcard: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CERT_ISSUANCE_STEPS = [
|
||||||
|
{ name: 'prepare', description: 'Creating ACME order', percentage: 10 },
|
||||||
|
{ name: 'authorize', description: 'Solving ACME challenges', percentage: 40 },
|
||||||
|
{ name: 'finalize', description: 'Finalizing and getting cert', percentage: 30 },
|
||||||
|
{ name: 'store', description: 'Storing certificate', percentage: 20 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the options for the class @see SmartAcme
|
* the options for the class @see SmartAcme
|
||||||
*/
|
*/
|
||||||
@@ -38,6 +54,21 @@ export interface ISmartAcmeOptions {
|
|||||||
* Defaults to ['dns-01'] or first supported type from handlers.
|
* Defaults to ['dns-01'] or first supported type from handlers.
|
||||||
*/
|
*/
|
||||||
challengePriority?: string[];
|
challengePriority?: string[];
|
||||||
|
/**
|
||||||
|
* Maximum number of concurrent ACME issuances across all domains.
|
||||||
|
* Defaults to 5.
|
||||||
|
*/
|
||||||
|
maxConcurrentIssuances?: number;
|
||||||
|
/**
|
||||||
|
* Maximum ACME orders allowed within the sliding window.
|
||||||
|
* Defaults to 250 (conservative limit under Let's Encrypt's 300/3h).
|
||||||
|
*/
|
||||||
|
maxOrdersPerWindow?: number;
|
||||||
|
/**
|
||||||
|
* Sliding window duration in milliseconds for rate limiting.
|
||||||
|
* Defaults to 3 hours (10_800_000 ms).
|
||||||
|
*/
|
||||||
|
orderWindowMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,12 +106,21 @@ export class SmartAcme {
|
|||||||
private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = [];
|
private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = [];
|
||||||
// priority order of challenge types
|
// priority order of challenge types
|
||||||
private challengePriority: string[];
|
private challengePriority: string[];
|
||||||
// Map for coordinating concurrent certificate requests
|
// TaskManager for coordinating concurrent certificate requests
|
||||||
private interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
private taskManager: plugins.taskbuffer.TaskManager;
|
||||||
|
// Single reusable task for certificate issuance
|
||||||
|
private certIssuanceTask: plugins.taskbuffer.Task<undefined, typeof CERT_ISSUANCE_STEPS>;
|
||||||
// bound signal handlers so they can be removed on stop()
|
// bound signal handlers so they can be removed on stop()
|
||||||
private boundSigintHandler: (() => void) | null = null;
|
private boundSigintHandler: (() => void) | null = null;
|
||||||
private boundSigtermHandler: (() => void) | null = null;
|
private boundSigtermHandler: (() => void) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the aggregated task event stream for observing certificate issuance progress.
|
||||||
|
*/
|
||||||
|
public get certIssuanceEvents(): plugins.taskbuffer.TaskManager['taskSubject'] {
|
||||||
|
return this.taskManager.taskSubject;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -105,8 +145,60 @@ 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);
|
// ── TaskManager setup ──────────────────────────────────────────────────
|
||||||
|
this.taskManager = new plugins.taskbuffer.TaskManager();
|
||||||
|
|
||||||
|
// Constraint 1: Per-domain mutex — one issuance at a time per TLD, with result sharing
|
||||||
|
const certDomainMutex = new plugins.taskbuffer.TaskConstraintGroup({
|
||||||
|
name: 'cert-domain-mutex',
|
||||||
|
maxConcurrent: 1,
|
||||||
|
resultSharingMode: 'share-latest',
|
||||||
|
constraintKeyForExecution: (_task, input?: ICertIssuanceInput) => {
|
||||||
|
return input?.certDomainName ?? null;
|
||||||
|
},
|
||||||
|
shouldExecute: async (_task, input?: ICertIssuanceInput) => {
|
||||||
|
if (!input?.certDomainName || !this.certmanager) return true;
|
||||||
|
// Safety net: if a valid cert is already cached, skip re-issuance
|
||||||
|
const existing = await this.certmanager.retrieveCertificate(input.certDomainName);
|
||||||
|
if (existing && !existing.shouldBeRenewed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Constraint 2: Global concurrency cap
|
||||||
|
const acmeGlobalConcurrency = new plugins.taskbuffer.TaskConstraintGroup({
|
||||||
|
name: 'acme-global-concurrency',
|
||||||
|
maxConcurrent: optionsArg.maxConcurrentIssuances ?? 5,
|
||||||
|
constraintKeyForExecution: () => 'global',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Constraint 3: Account-level rate limiting
|
||||||
|
const acmeAccountRateLimit = new plugins.taskbuffer.TaskConstraintGroup({
|
||||||
|
name: 'acme-account-rate-limit',
|
||||||
|
rateLimit: {
|
||||||
|
maxPerWindow: optionsArg.maxOrdersPerWindow ?? 250,
|
||||||
|
windowMs: optionsArg.orderWindowMs ?? 10_800_000,
|
||||||
|
},
|
||||||
|
constraintKeyForExecution: () => 'account',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.taskManager.addConstraintGroup(certDomainMutex);
|
||||||
|
this.taskManager.addConstraintGroup(acmeGlobalConcurrency);
|
||||||
|
this.taskManager.addConstraintGroup(acmeAccountRateLimit);
|
||||||
|
|
||||||
|
// Create the single reusable certificate issuance task
|
||||||
|
this.certIssuanceTask = new plugins.taskbuffer.Task({
|
||||||
|
name: 'cert-issuance',
|
||||||
|
steps: CERT_ISSUANCE_STEPS,
|
||||||
|
taskFunction: async (input: ICertIssuanceInput) => {
|
||||||
|
return this.performCertificateIssuance(input);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.taskManager.addTask(this.certIssuanceTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,6 +241,10 @@ export class SmartAcme {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
contact: [`mailto:${this.options.accountEmail}`],
|
contact: [`mailto:${this.options.accountEmail}`],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start the task manager
|
||||||
|
await this.taskManager.start();
|
||||||
|
|
||||||
// Setup graceful shutdown handlers (store references for removal in stop())
|
// Setup graceful shutdown handlers (store references for removal in stop())
|
||||||
this.boundSigintHandler = () => this.handleSignal('SIGINT');
|
this.boundSigintHandler = () => this.handleSignal('SIGINT');
|
||||||
this.boundSigtermHandler = () => this.handleSignal('SIGTERM');
|
this.boundSigtermHandler = () => this.handleSignal('SIGTERM');
|
||||||
@@ -169,6 +265,8 @@ export class SmartAcme {
|
|||||||
process.removeListener('SIGTERM', this.boundSigtermHandler);
|
process.removeListener('SIGTERM', this.boundSigtermHandler);
|
||||||
this.boundSigtermHandler = null;
|
this.boundSigtermHandler = null;
|
||||||
}
|
}
|
||||||
|
// Stop the task manager
|
||||||
|
await this.taskManager.stop();
|
||||||
// Destroy ACME HTTP transport (closes keep-alive sockets)
|
// Destroy ACME HTTP transport (closes keep-alive sockets)
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
this.client.destroy();
|
this.client.destroy();
|
||||||
@@ -255,8 +353,7 @@ export class SmartAcme {
|
|||||||
* * if not in the database announce it
|
* * if not in the database announce it
|
||||||
* * then get it from letsencrypt
|
* * then get it from letsencrypt
|
||||||
* * store it
|
* * store it
|
||||||
* * remove it from the pending map (which it go onto by announcing it)
|
* * retrieve it from the database and return it
|
||||||
* * retrieve it from the databse and return it
|
|
||||||
*
|
*
|
||||||
* @param domainArg
|
* @param domainArg
|
||||||
* @param options Optional configuration for certificate generation
|
* @param options Optional configuration for certificate generation
|
||||||
@@ -284,35 +381,59 @@ export class SmartAcme {
|
|||||||
// Retrieve any existing certificate record by base domain.
|
// Retrieve any existing certificate record by base domain.
|
||||||
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
||||||
|
|
||||||
if (
|
if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
|
||||||
!retrievedCertificate &&
|
|
||||||
(await this.interestMap.checkInterest(certDomainName))
|
|
||||||
) {
|
|
||||||
const existingCertificateInterest = this.interestMap.findInterest(certDomainName);
|
|
||||||
const certificate = existingCertificateInterest.interestFullfilled;
|
|
||||||
return certificate;
|
|
||||||
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
|
|
||||||
return retrievedCertificate;
|
return retrievedCertificate;
|
||||||
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
|
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
|
||||||
// Remove old certificate via certManager
|
// Remove old certificate via certManager
|
||||||
await this.certmanager.deleteCertificate(certDomainName);
|
await this.certmanager.deleteCertificate(certDomainName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// lets make sure others get the same interest
|
// Build issuance input and trigger the constrained task
|
||||||
const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
|
const issuanceInput: ICertIssuanceInput = {
|
||||||
|
certDomainName,
|
||||||
|
domainArg,
|
||||||
|
isWildcardRequest,
|
||||||
|
includeWildcard: options?.includeWildcard ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.taskManager.triggerTaskConstrained(
|
||||||
|
this.certIssuanceTask,
|
||||||
|
issuanceInput,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we got a cert directly (either from execution or result sharing), return it
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If shouldExecute returned false (cert appeared in cache), read from cache
|
||||||
|
const cachedCert = await this.certmanager.retrieveCertificate(certDomainName);
|
||||||
|
if (cachedCert) {
|
||||||
|
return cachedCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Certificate issuance failed for ${certDomainName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the actual ACME certificate issuance flow.
|
||||||
|
* Called by the certIssuanceTask's taskFunction.
|
||||||
|
*/
|
||||||
|
private async performCertificateIssuance(input: ICertIssuanceInput): Promise<SmartacmeCert> {
|
||||||
|
const { certDomainName, isWildcardRequest, includeWildcard } = input;
|
||||||
|
|
||||||
|
// ── Step: prepare ─────────────────────────────────────────────────────
|
||||||
|
this.certIssuanceTask.notifyStep('prepare');
|
||||||
|
|
||||||
// Build identifiers array based on request
|
// Build identifiers array based on request
|
||||||
const identifiers = [];
|
const identifiers: Array<{ type: 'dns'; value: string }> = [];
|
||||||
|
|
||||||
if (isWildcardRequest) {
|
if (isWildcardRequest) {
|
||||||
// If requesting a wildcard directly, only add the wildcard
|
|
||||||
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
|
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
|
||||||
} else {
|
} else {
|
||||||
// Add the regular domain
|
|
||||||
identifiers.push({ type: 'dns', value: certDomainName });
|
identifiers.push({ type: 'dns', value: certDomainName });
|
||||||
|
|
||||||
// Only add wildcard if explicitly requested
|
if (includeWildcard) {
|
||||||
if (options?.includeWildcard) {
|
|
||||||
const hasDnsHandler = this.challengeHandlers.some((h) =>
|
const hasDnsHandler = this.challengeHandlers.some((h) =>
|
||||||
h.getSupportedTypes().includes('dns-01'),
|
h.getSupportedTypes().includes('dns-01'),
|
||||||
);
|
);
|
||||||
@@ -329,6 +450,9 @@ export class SmartAcme {
|
|||||||
identifiers,
|
identifiers,
|
||||||
}), 'createOrder');
|
}), 'createOrder');
|
||||||
|
|
||||||
|
// ── Step: authorize ───────────────────────────────────────────────────
|
||||||
|
this.certIssuanceTask.notifyStep('authorize');
|
||||||
|
|
||||||
/* Get authorizations and select challenges */
|
/* Get authorizations and select challenges */
|
||||||
const authorizations = await this.retry(() => this.client.getAuthorizations(order), 'getAuthorizations');
|
const authorizations = await this.retry(() => this.client.getAuthorizations(order), 'getAuthorizations');
|
||||||
|
|
||||||
@@ -352,45 +476,37 @@ export class SmartAcme {
|
|||||||
}
|
}
|
||||||
const { type, handler } = selectedHandler;
|
const { type, handler } = selectedHandler;
|
||||||
// build handler input with keyAuthorization
|
// build handler input with keyAuthorization
|
||||||
let input: any;
|
let challengeInput: any;
|
||||||
// retrieve keyAuthorization for challenge
|
// retrieve keyAuthorization for challenge
|
||||||
const keyAuth = await this.client.getChallengeKeyAuthorization(selectedChallengeArg);
|
const keyAuth = await this.client.getChallengeKeyAuthorization(selectedChallengeArg);
|
||||||
if (type === 'dns-01') {
|
if (type === 'dns-01') {
|
||||||
input = { type, hostName: `_acme-challenge.${authz.identifier.value}`, challenge: keyAuth };
|
challengeInput = { type, hostName: `_acme-challenge.${authz.identifier.value}`, challenge: keyAuth };
|
||||||
} else if (type === 'http-01') {
|
} else if (type === 'http-01') {
|
||||||
// HTTP-01 requires serving token at webPath
|
challengeInput = {
|
||||||
input = {
|
|
||||||
type,
|
type,
|
||||||
token: (selectedChallengeArg as any).token,
|
token: (selectedChallengeArg as any).token,
|
||||||
keyAuthorization: keyAuth,
|
keyAuthorization: keyAuth,
|
||||||
webPath: `/.well-known/acme-challenge/${(selectedChallengeArg as any).token}`,
|
webPath: `/.well-known/acme-challenge/${(selectedChallengeArg as any).token}`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// generic challenge input: include raw challenge properties
|
challengeInput = { type, keyAuthorization: keyAuth, ...selectedChallengeArg };
|
||||||
input = { type, keyAuthorization: keyAuth, ...selectedChallengeArg };
|
|
||||||
}
|
}
|
||||||
this.pendingChallenges.push(input);
|
this.pendingChallenges.push(challengeInput);
|
||||||
try {
|
try {
|
||||||
// Prepare the challenge (set DNS record, write file, etc.)
|
await this.retry(() => handler.prepare(challengeInput), `${type}.prepare`);
|
||||||
await this.retry(() => handler.prepare(input), `${type}.prepare`);
|
|
||||||
// For DNS-01, wait for propagation before verification
|
|
||||||
if (type === 'dns-01') {
|
if (type === 'dns-01') {
|
||||||
const dnsInput = input as { hostName: string; challenge: string };
|
const dnsInput = challengeInput as { hostName: string; challenge: string };
|
||||||
// Wait for authoritative DNS propagation before ACME verify
|
|
||||||
await this.retry(
|
await this.retry(
|
||||||
() => this.smartdns.checkUntilAvailable(dnsInput.hostName, 'TXT', dnsInput.challenge, 100, 5000),
|
() => this.smartdns.checkUntilAvailable(dnsInput.hostName, 'TXT', dnsInput.challenge, 100, 5000),
|
||||||
`${type}.propagation`,
|
`${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');
|
this.logger.log('info', 'Cooling down for 1 minute before ACME verification');
|
||||||
await plugins.smartdelay.delayFor(60000);
|
await plugins.smartdelay.delayFor(60000);
|
||||||
}
|
}
|
||||||
// Notify ACME server to complete the challenge
|
|
||||||
await this.retry(
|
await this.retry(
|
||||||
() => this.client.completeChallenge(selectedChallengeArg),
|
() => this.client.completeChallenge(selectedChallengeArg),
|
||||||
`${type}.completeChallenge`,
|
`${type}.completeChallenge`,
|
||||||
);
|
);
|
||||||
// Wait for valid status (warnings on staging timeouts)
|
|
||||||
try {
|
try {
|
||||||
await this.retry(
|
await this.retry(
|
||||||
() => this.client.waitForValidStatus(selectedChallengeArg),
|
() => this.client.waitForValidStatus(selectedChallengeArg),
|
||||||
@@ -404,30 +520,28 @@ export class SmartAcme {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Always cleanup resource
|
|
||||||
try {
|
try {
|
||||||
await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
|
await this.retry(() => handler.cleanup(challengeInput), `${type}.cleanup`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await this.logger.log('error', `Error during ${type}.cleanup`, err);
|
await this.logger.log('error', `Error during ${type}.cleanup`, err);
|
||||||
} finally {
|
} finally {
|
||||||
this.pendingChallenges = this.pendingChallenges.filter((c) => c !== input);
|
this.pendingChallenges = this.pendingChallenges.filter((c) => c !== challengeInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Finalize order */
|
// ── Step: finalize ────────────────────────────────────────────────────
|
||||||
const csrDomains = [];
|
this.certIssuanceTask.notifyStep('finalize');
|
||||||
|
|
||||||
|
const csrDomains: string[] = [];
|
||||||
let commonName: string;
|
let commonName: string;
|
||||||
|
|
||||||
if (isWildcardRequest) {
|
if (isWildcardRequest) {
|
||||||
// For wildcard requests, use wildcard as common name
|
|
||||||
commonName = `*.${certDomainName}`;
|
commonName = `*.${certDomainName}`;
|
||||||
csrDomains.push(certDomainName); // Add base domain as alt name
|
csrDomains.push(certDomainName);
|
||||||
} else {
|
} else {
|
||||||
// For regular requests, use base domain as common name
|
|
||||||
commonName = certDomainName;
|
commonName = certDomainName;
|
||||||
if (options?.includeWildcard && identifiers.some(id => id.value === `*.${certDomainName}`)) {
|
if (includeWildcard && identifiers.some(id => id.value === `*.${certDomainName}`)) {
|
||||||
// If wildcard was successfully added, include it as alt name
|
|
||||||
csrDomains.push(`*.${certDomainName}`);
|
csrDomains.push(`*.${certDomainName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,9 +554,9 @@ export class SmartAcme {
|
|||||||
await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
|
await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');
|
||||||
const cert = await this.retry(() => this.client.getCertificate(order), 'getCertificate');
|
const cert = await this.retry(() => this.client.getCertificate(order), 'getCertificate');
|
||||||
|
|
||||||
/* Done */
|
// ── Step: store ───────────────────────────────────────────────────────
|
||||||
|
this.certIssuanceTask.notifyStep('store');
|
||||||
|
|
||||||
// Store the new certificate record
|
|
||||||
const certRecord = new SmartacmeCert({
|
const certRecord = new SmartacmeCert({
|
||||||
id: plugins.smartunique.shortId(),
|
id: plugins.smartunique.shortId(),
|
||||||
domainName: certDomainName,
|
domainName: certDomainName,
|
||||||
@@ -455,9 +569,7 @@ export class SmartAcme {
|
|||||||
await this.certmanager.storeCertificate(certRecord);
|
await this.certmanager.storeCertificate(certRecord);
|
||||||
|
|
||||||
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
||||||
currentDomainInterst.fullfillInterest(newCertificate);
|
return newCertificate ?? certRecord;
|
||||||
currentDomainInterst.destroy();
|
|
||||||
return newCertificate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user