Compare commits

..

14 Commits

Author SHA1 Message Date
758c6c6b5d 7.0.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 54s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 17:27:17 +00:00
6363ec4be6 BREAKING CHANGE(SmartAcme (Cert Management)): Refactor certificate management and challenge handling API to use a unified certManager interface, remove legacy storage, and update challenge workflows. 2025-04-30 17:27:17 +00:00
6a53346d14 6.2.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 54s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 14:55:03 +00:00
fc420eb615 feat(handlers): Add in-memory HTTP-01 challenge handler and rename file-based handler to Http01Webroot 2025-04-30 14:55:03 +00:00
9f66a0487f 6.1.3
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 53s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 12:59:20 +00:00
40cae220d0 fix(Dns01Handler): Update dependency versions and refine Dns01Handler implementation 2025-04-30 12:59:20 +00:00
f7dccb25e4 6.1.2
Some checks failed
Default (tags) / security (push) Successful in 22s
Default (tags) / test (push) Failing after 51s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-27 14:51:22 +00:00
da75c52c09 fix(repo): Update repository metadata by replacing the LICENSE file with a license.md file for improved consistency. 2025-04-27 14:51:22 +00:00
708145c550 6.1.1
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 51s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-27 14:50:28 +00:00
0de2178eb5 fix(readme): Fix license link reference in documentation 2025-04-27 14:50:28 +00:00
5183d88b69 6.1.0
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 50s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-27 14:39:59 +00:00
d5e7e11256 feat(readme): Update documentation with detailed built-in challenge handlers and custom handler examples 2025-04-27 14:39:59 +00:00
916ac9ff8c 6.0.1 2025-04-27 14:30:53 +00:00
b7005f360c fix(readme): Remove extraneous code fence markers from license section in readme 2025-04-27 14:30:53 +00:00
23 changed files with 655 additions and 249 deletions

View File

@ -1,5 +1,54 @@
# Changelog
## 2025-04-30 - 7.0.0 - BREAKING CHANGE(SmartAcme (Cert Management))
Refactor certificate management and challenge handling API to use a unified certManager interface, remove legacy storage, and update challenge workflows.
- Introduce ICertManager interface with MemoryCertManager and MongoCertManager implementations.
- Remove the legacy SmartacmeCertManager and update SmartAcme to require a certManager option instead of mongoDescriptor.
- Adjust certificate renewal logic to delete and store certificates through the new certManager API.
- Refine DNS-01 challenge handling by removing in-handler DNS propagation waiting and relying on external checks.
- Increase retry settings for robustness during challenge verification and certificate issuance.
- Update integration and unit tests to use the new certManager configuration.
## 2025-04-30 - 6.2.0 - feat(handlers)
Add in-memory HTTP-01 challenge handler and rename file-based handler to Http01Webroot
- Renamed Http01Handler to Http01Webroot in both implementation and documentation
- Introduced Http01MemoryHandler for diskless HTTP-01 challenges
- Updated tests and README examples to reflect handler name changes and new feature
## 2025-04-30 - 6.1.3 - fix(Dns01Handler)
Update dependency versions and refine Dns01Handler implementation
- Bump '@apiclient.xyz/cloudflare' to ^6.4.1 and '@tsclass/tsclass' to ^9.1.0 in package.json
- Remove duplicate Cloudflare import in smartacme.plugins.ts
- Refactor Dns01Handler to use IConvenientDnsProvider and add checkWetherDomainIsSupported method
- Align devDependencies versions for improved consistency
## 2025-04-27 - 6.1.2 - fix(repo)
Update repository metadata by replacing the LICENSE file with a license.md file for improved consistency.
- Removed the old LICENSE file.
- Introduced license.md as the new license documentation file.
## 2025-04-27 - 6.1.1 - fix(readme)
Fix license link reference in documentation
- Updated the license link from [license](license) to [license.md](license.md) in the License and Legal Information section
## 2025-04-27 - 6.1.0 - feat(readme)
Update documentation with detailed built-in challenge handlers and custom handler examples
- Expanded readme to include sections on Dns01Handler and Http01Handler usage
- Added examples for creating and registering custom ACME challenge handlers
- Improved clarity of ACME certificate management instructions using SmartAcme
## 2025-04-27 - 6.0.1 - fix(readme)
Remove extraneous code fence markers from license section in readme
- Removed unnecessary triple backticks wrapping the license information
- Improved clarity of the license section in the documentation
## 2025-04-27 - 6.0.0 - BREAKING CHANGE(SmartAcme)
Refactor challenge handling by removing legacy setChallenge/removeChallenge in favor of pluggable challengeHandlers and update documentation and tests accordingly

View File

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartacme",
"version": "6.0.0",
"version": "7.0.0",
"private": false,
"description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
"main": "dist_ts/index.js",
@ -40,6 +40,7 @@
"homepage": "https://code.foss.global/push.rocks/smartacme#readme",
"dependencies": {
"@api.global/typedserver": "^3.0.74",
"@apiclient.xyz/cloudflare": "^6.4.1",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdata": "^5.15.1",
"@push.rocks/smartdelay": "^3.0.5",
@ -50,17 +51,16 @@
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^9.0.0",
"@tsclass/tsclass": "^9.1.0",
"acme-client": "^5.4.0"
},
"devDependencies": {
"@apiclient.xyz/cloudflare": "^6.3.2",
"@git.zone/tsbuild": "^2.3.2",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.96",
"@push.rocks/qenv": "^6.1.0",
"@push.rocks/tapbundle": "^5.6.3",
"@types/node": "^22.15.2"
"@push.rocks/tapbundle": "^6.0.0",
"@types/node": "^22.15.3"
},
"files": [
"ts/**/*",

154
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
'@api.global/typedserver':
specifier: ^3.0.74
version: 3.0.74
'@apiclient.xyz/cloudflare':
specifier: ^6.4.1
version: 6.4.1
'@push.rocks/lik':
specifier: ^6.2.2
version: 6.2.2
@ -42,15 +45,12 @@ importers:
specifier: ^3.0.9
version: 3.0.9
'@tsclass/tsclass':
specifier: ^9.0.0
version: 9.0.0
specifier: ^9.1.0
version: 9.1.0
acme-client:
specifier: ^5.4.0
version: 5.4.0
devDependencies:
'@apiclient.xyz/cloudflare':
specifier: ^6.3.2
version: 6.3.2
'@git.zone/tsbuild':
specifier: ^2.3.2
version: 2.3.2
@ -64,11 +64,11 @@ importers:
specifier: ^6.1.0
version: 6.1.0
'@push.rocks/tapbundle':
specifier: ^5.6.3
version: 5.6.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
specifier: ^6.0.0
version: 6.0.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
'@types/node':
specifier: ^22.15.2
version: 22.15.2
specifier: ^22.15.3
version: 22.15.3
packages:
@ -87,8 +87,8 @@ packages:
'@api.global/typedsocket@3.0.1':
resolution: {integrity: sha512-xojiAVNXtHoxkpBo8U2HHJG8FrVXXuLvDNndSHXwx4C9VslUwDn5zSCI+PdBl8iAg+ZuBmKjqkpZZ9sL6DC5yQ==}
'@apiclient.xyz/cloudflare@6.3.2':
resolution: {integrity: sha512-u5ud25tR1epNVgAPtL2t1qZ7FOGsLhID4zAzwcIQQTqmBb43US0fkI/I+JjIW0uyHi12AI4gWez2ke2nAR4+pw==}
'@apiclient.xyz/cloudflare@6.4.1':
resolution: {integrity: sha512-RYFphnbunjK+Imq/3ynIQpAvIGBJ38kqSZ2nrpTm26zsBIxW7S6xEe3zhXfVMtUIgC99OL3Xr/SGXl3CNBwCug==}
'@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
@ -821,6 +821,9 @@ packages:
'@push.rocks/smartexpect@1.6.1':
resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==}
'@push.rocks/smartexpect@2.2.2':
resolution: {integrity: sha512-s2zJlLc6Wub7P/jgKSM51kW2UjslxQwx2BXoyJVO95OgiOwarde0AuxPR0lfRA/FvHdBfTmJf4upiWtcjYMB/Q==}
'@push.rocks/smartfeed@1.0.11':
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
@ -956,6 +959,9 @@ packages:
'@push.rocks/tapbundle@5.6.3':
resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==}
'@push.rocks/tapbundle@6.0.0':
resolution: {integrity: sha512-ARIs189TysvI8EsPAC7LH6O0WbBYI9E7XxdihwmM6LRgLvzAbp1agfO6lOjpKrAYWKjT3KdlUEihilxOBrgTYQ==}
'@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@ -1336,8 +1342,8 @@ packages:
'@tsclass/tsclass@8.2.1':
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
'@tsclass/tsclass@9.0.0':
resolution: {integrity: sha512-QuV2WKzi3p1ONq0UR+hNulG62D6vRPJxOXunWvN9zpWx6Uj70DKntMu8nqEIWUPgL3UKIPe7GN8l6mPCdxdcEg==}
'@tsclass/tsclass@9.1.0':
resolution: {integrity: sha512-PkG1bXK/bqVtxaRHje+iJHjtcdRHLHrNTOkzqh+jv2A7mgiyNo2YBJIl4eEJLkw1X3FwEFU4vCAtsegSmJgRug==}
'@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@ -1499,8 +1505,8 @@ packages:
'@types/node@18.19.87':
resolution: {integrity: sha512-OIAAu6ypnVZHmsHCeJ+7CCSub38QNBS9uceMQeg7K5Ur0Jr+wG9wEOEvvMbhp09pxD5czIUy/jND7s7Tb6Nw7A==}
'@types/node@22.15.2':
resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==}
'@types/node@22.15.3':
resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==}
'@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
@ -4189,6 +4195,10 @@ packages:
resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==}
engines: {node: '>=16'}
type-fest@4.40.1:
resolution: {integrity: sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==}
engines: {node: '>=16'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -4510,14 +4520,14 @@ snapshots:
- utf-8-validate
- vue
'@apiclient.xyz/cloudflare@6.3.2':
'@apiclient.xyz/cloudflare@6.4.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.0.15
'@tsclass/tsclass': 5.0.0
'@tsclass/tsclass': 9.1.0
cloudflare: 4.2.0
transitivePeerDependencies:
- encoding
@ -5399,7 +5409,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/yargs': 17.0.33
chalk: 4.1.2
@ -5820,6 +5830,12 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
fast-deep-equal: 3.1.3
'@push.rocks/smartexpect@2.2.2':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3
fast-deep-equal: 3.1.3
'@push.rocks/smartfeed@1.0.11':
dependencies:
'@tsclass/tsclass': 3.0.48
@ -6224,6 +6240,38 @@ snapshots:
- supports-color
- utf-8-validate
'@push.rocks/tapbundle@6.0.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)':
dependencies:
'@open-wc/testing': 4.0.0
'@push.rocks/consolecolor': 2.0.2
'@push.rocks/qenv': 6.1.0
'@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12
'@push.rocks/smartexpect': 2.2.2
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
'@push.rocks/smartpath': 5.0.18
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@push.rocks/smarts3': 2.2.5
'@push.rocks/smartshell': 3.2.3
'@push.rocks/smarttime': 4.1.1
expect: 29.7.0
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- aws-crt
- bufferutil
- gcp-metadata
- kerberos
- mongodb-client-encryption
- snappy
- socks
- supports-color
- utf-8-validate
'@push.rocks/taskbuffer@3.1.7':
dependencies:
'@push.rocks/lik': 6.2.2
@ -6800,24 +6848,24 @@ snapshots:
dependencies:
type-fest: 4.40.0
'@tsclass/tsclass@9.0.0':
'@tsclass/tsclass@9.1.0':
dependencies:
type-fest: 4.40.0
type-fest: 4.40.1
'@types/accepts@1.3.7':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/babel__code-frame@7.0.6': {}
'@types/bn.js@5.1.6':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/buffer-json@2.0.3': {}
@ -6833,17 +6881,17 @@ snapshots:
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
source-map: 0.6.1
'@types/co-body@6.1.3':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/qs': 6.9.18
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/content-disposition@0.5.8': {}
@ -6854,11 +6902,11 @@ snapshots:
'@types/connect': 3.4.38
'@types/express': 5.0.1
'@types/keygrip': 1.0.6
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/cors@2.8.17':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/debounce@1.2.4': {}
@ -6872,7 +6920,7 @@ snapshots:
'@types/dns-packet@5.6.5':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/elliptic@6.4.18':
dependencies:
@ -6880,7 +6928,7 @@ snapshots:
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@ -6897,30 +6945,30 @@ snapshots:
'@types/from2@2.3.5':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/gunzip-maybe@1.4.2':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/hast@3.0.4':
dependencies:
@ -6954,7 +7002,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/keygrip@1.0.6': {}
@ -6971,7 +7019,7 @@ snapshots:
'@types/http-errors': 2.0.4
'@types/keygrip': 1.0.6
'@types/koa-compose': 3.2.8
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/mdast@4.0.4':
dependencies:
@ -6989,18 +7037,18 @@ snapshots:
'@types/node-fetch@2.6.12':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
form-data: 4.0.2
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/node@18.19.87':
dependencies:
undici-types: 5.26.5
'@types/node@22.15.2':
'@types/node@22.15.3':
dependencies:
undici-types: 6.21.0
@ -7018,19 +7066,19 @@ snapshots:
'@types/s3rver@3.7.4':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/semver@7.7.0': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/send': 0.17.4
'@types/sinon-chai@3.2.12':
@ -7050,11 +7098,11 @@ snapshots:
'@types/tar-stream@2.2.3':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/through2@2.0.41':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/triple-beam@1.3.5': {}
@ -7078,18 +7126,18 @@ snapshots:
'@types/whatwg-url@8.2.2':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {}
'@types/ws@7.4.7':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
'@types/yargs-parser@21.0.3': {}
@ -7099,7 +7147,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 22.15.2
'@types/node': 22.15.3
optional: true
'@ungap/structured-clone@1.3.0': {}
@ -7775,7 +7823,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.17
'@types/node': 22.15.2
'@types/node': 22.15.3
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@ -8579,7 +8627,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.15.2
'@types/node': 22.15.3
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -10172,6 +10220,8 @@ snapshots:
type-fest@4.40.0: {}
type-fest@4.40.1: {}
type-is@1.6.18:
dependencies:
media-typer: 0.3.0

View File

@ -61,7 +61,7 @@ const smartAcmeInstance = new SmartAcme({
retryOptions: {}, // optional retry/backoff settings
challengeHandlers: [
new Dns01Handler(cfAccount),
// you can add more handlers, e.g. Http01Handler
// you can add more handlers, e.g. Http01Webroot
],
challengePriority: ['dns-01'], // optional ordering of challenge types
});
@ -138,8 +138,89 @@ async function main() {
await smartAcmeInstance.stop();
}
main().catch(console.error);
```
main().catch(console.error);
```
## Built-in Challenge Handlers
This module includes three out-of-the-box ACME challenge handlers:
- **Dns01Handler**
- Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation.
- Import path:
```typescript
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
```
- Example:
```typescript
import * as cloudflare from '@apiclient.xyz/cloudflare';
const cfAccount = new cloudflare.CloudflareAccount('CF_TOKEN');
const dnsHandler = new Dns01Handler(cfAccount);
```
- **Http01Webroot**
- Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup.
- Import path:
```typescript
import { Http01Webroot } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js';
```
- Example:
```typescript
const httpHandler = new Http01Webroot({ webroot: '/var/www/html' });
```
- **Http01MemoryHandler**
- In-memory HTTP-01 challenge handler that stores and serves ACME tokens without disk I/O.
- Import path:
```typescript
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
```
- Example (Express integration):
```typescript
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
const memoryHandler = new Http01MemoryHandler();
app.use((req, res, next) => memoryHandler.handleRequest(req, res, next));
```
All handlers implement the `IChallengeHandler<T>` interface and can be combined in the `challengeHandlers` array.
## Creating Custom Handlers
To support additional challenge types or custom validation flows, implement the `IChallengeHandler<T>` interface:
```typescript
import type { IChallengeHandler } from '@push.rocks/smartacme/ts/handlers/IChallengeHandler.js';
// Define your custom challenge payload type
interface MyChallenge { type: string; /* ... */ }
class MyCustomHandler implements IChallengeHandler<MyChallenge> {
getSupportedTypes(): string[] {
return ['my-01'];
}
// Prepare the challenge (set DNS records, start servers, etc.)
async prepare(ch: MyChallenge): Promise<void> {
// preparation logic
}
// Optional verify step after prepare
async verify?(ch: MyChallenge): Promise<void> {
// verification logic
}
// Cleanup after challenge (remove records, stop servers)
async cleanup(ch: MyChallenge): Promise<void> {
// cleanup logic
}
}
// Then register your handler:
const customInstance = new SmartAcme({
/* other options */,
challengeHandlers: [ new MyCustomHandler() ],
challengePriority: ['my-01'],
});
In this example, `Qenv` is used to manage environment variables, and `cloudflare` library is used to handle DNS challenges required by Let's Encrypt ACME protocol. The `setChallenge` and `removeChallenge` methods are essential for automating the DNS challenge process, which is a key part of domain validation.
@ -237,11 +318,9 @@ This comprehensive guide ensures you can set up, manage, and test ACME certifica
---
```
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license.md](license.md) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@ -257,4 +336,3 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
```

44
readme.plan.md Normal file
View File

@ -0,0 +1,44 @@
# Plan: Diskless HTTP-01 Handler and Renaming Existing Handler
This plan outlines steps to rename the existing filesystem-based HTTP-01 handler to `Http01Webroot`
and introduce a new diskless (in-memory) HTTP-01 handler for integration with arbitrary HTTP servers
(e.g., Express).
## 1. Rename existing handler to Http01Webroot
- In `ts/handlers/Http01Handler.ts`:
- Rename `Http01HandlerOptions` to `Http01WebrootOptions`.
- Rename class `Http01Handler` to `Http01Webroot`.
- Remove the legacy alias; rename the handler directly.
- In `ts/handlers/index.ts`:
- Export `Http01Webroot` under its new name.
- Remove any `Http01Handler` export.
- Update existing tests (e.g., `test.handlers-http01.ts`) to import `Http01Webroot` instead of `Http01Handler`.
## 2. Add new diskless (in-memory) HTTP-01 handler
- Create `ts/handlers/Http01MemoryHandler.ts`:
- Implement `IChallengeHandler<{ token: string; keyAuthorization: string; webPath: string }>`, storing challenges in a private `Map<string, string>`.
- `prepare()`: add token→keyAuthorization mapping.
- `verify()`: no-op.
- `cleanup()`: remove mapping.
- Add `handleRequest(req, res, next?)` method:
- Parse `/.well-known/acme-challenge/:token` from `req.url`.
- If token exists, respond with the key authorization and status 200.
- If missing and `next` provided, call `next()`, otherwise respond 404.
- Export `Http01MemoryHandler` in `ts/handlers/index.ts`.
## 3. Write tests for Http01MemoryHandler
- Create `test/test.handlers-http01-memory.ts`:
- Use `tap` and `expect` to:
1. `prepare()` a challenge.
2. Invoke `handleRequest()` with a fake `req`/`res` to confirm 200 and correct body.
3. `cleanup()` the challenge.
4. Confirm `handleRequest()` now yields 404.
## 4. Update documentation
- Add examples in `readme.md` showing how to use both `Http01Webroot` and the new `Http01MemoryHandler`:
- Sample code for Express integration using `handleRequest`.
## 5. Build and test
- Run `pnpm build` and `pnpm test`, ensuring existing tests are updated for `Http01Webroot` and new tests pass.
Please review and let me know if this plan makes sense before proceeding with implementation.

View File

@ -7,15 +7,11 @@ tap.test('Dns01Handler prepare and cleanup calls Cloudflare and DNS functions',
// fake Cloudflare API
const fakeCF: any = {
convenience: {
acmeSetDnsChallenge: async (ch: any) => {
acmeSetDnsChallenge: async (_ch: any) => {
setCalled = true;
expect(ch).toHaveProperty('hostName');
expect(ch).toHaveProperty('challenge');
},
acmeRemoveDnsChallenge: async (ch: any) => {
acmeRemoveDnsChallenge: async (_ch: any) => {
removeCalled = true;
expect(ch).toHaveProperty('hostName');
expect(ch).toHaveProperty('challenge');
},
},
};

View File

@ -0,0 +1,58 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Http01MemoryHandler } from '../ts/handlers/Http01MemoryHandler.js';
tap.test('Http01MemoryHandler serves in-memory challenges and cleans up', async () => {
const handler = new Http01MemoryHandler();
const token = 'testtoken';
const keyAuth = 'keyAuthValue';
const webPath = `/.well-known/acme-challenge/${token}`;
const challenge = { type: 'http-01', token, keyAuthorization: keyAuth, webPath };
// Prepare challenge (store in memory)
await handler.prepare(challenge);
// Serve existing challenge without next()
const req1: any = { url: webPath };
const res1: any = {
statusCode: 0,
headers: {} as Record<string, string>,
body: '',
setHeader(name: string, value: string) { this.headers[name] = value; },
end(body?: string) { this.body = body || ''; },
};
handler.handleRequest(req1, res1);
expect(res1.statusCode).toEqual(200);
expect(res1.body).toEqual(keyAuth);
expect(res1.headers['content-type']).toEqual('text/plain');
// Cleanup challenge (remove from memory)
await handler.cleanup(challenge);
// Serve after cleanup without next() should give 404
const req2: any = { url: webPath };
const res2: any = {
statusCode: 0,
headers: {} as Record<string, string>,
body: '',
setHeader(name: string, value: string) { this.headers[name] = value; },
end(body?: string) { this.body = body || ''; },
};
handler.handleRequest(req2, res2);
expect(res2.statusCode).toEqual(404);
// Serve after cleanup with next() should call next
const req3: any = { url: webPath };
let nextCalled = false;
const next = () => { nextCalled = true; };
const res3: any = {
statusCode: 0,
headers: {} as Record<string, string>,
body: '',
setHeader(name: string, value: string) { this.headers[name] = value; },
end(body?: string) { this.body = body || ''; },
};
handler.handleRequest(req3, res3, next);
expect(nextCalled).toEqual(true);
});
export default tap.start();

View File

@ -1,13 +1,13 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Http01Handler } from '../ts/handlers/Http01Handler.js';
import { Http01Webroot } from '../ts/handlers/Http01Handler.js';
import { promises as fs } from 'fs';
import * as path from 'path';
import os from 'os';
tap.test('Http01Handler writes challenge file and removes it on cleanup', async () => {
tap.test('Http01Webroot writes challenge file and removes it on cleanup', async () => {
// create temporary webroot directory
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'http01-'));
const handler = new Http01Handler({ webroot: tmpDir });
const handler = new Http01Webroot({ webroot: tmpDir });
const token = 'testtoken';
const keyAuth = 'keyAuthValue';
const webPath = `/.well-known/acme-challenge/${token}`;

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as cloudflare from '@apiclient.xyz/cloudflare';
import { SmartAcme } from '../ts/index.js';
import { SmartAcme, MongoCertManager } from '../ts/index.js';
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
// Load environment variables for credentials (stored under .nogit/)
@ -19,7 +19,7 @@ let smartAcmeInstance: SmartAcme;
tap.test('create SmartAcme instance with DNS-01 handler and start', async () => {
smartAcmeInstance = new SmartAcme({
accountEmail: 'domains@lossless.org',
mongoDescriptor: { mongoDbName, mongoDbPass, mongoDbUrl },
certManager: new MongoCertManager({ mongoDbName, mongoDbPass, mongoDbUrl }),
environment: 'integration',
retryOptions: {},
challengeHandlers: [new Dns01Handler(cfAccount)],
@ -34,7 +34,7 @@ tap.test('get a domain certificate via DNS-01 challenge', async () => {
const domain = 'bleu.de';
const cert = await smartAcmeInstance.getCertificateForDomain(domain);
expect(cert).toHaveProperty('domainName');
expect(cert.domainName).toEqual(domain);
expect(cert).toEqual(domain);
expect(cert).toHaveProperty('publicKey');
expect(typeof cert.publicKey).toEqual('string');
expect(cert.publicKey.length).toBeGreaterThan(0);

View File

@ -1,5 +1,5 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme } from '../ts/index.js';
import { SmartAcme, MemoryCertManager } from '../ts/index.js';
import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js';
// Dummy handler for testing
@ -12,7 +12,7 @@ class DummyHandler implements IChallengeHandler<any> {
tap.test('constructor throws without challengeHandlers', async () => {
expect(() => new SmartAcme({
accountEmail: 'test@example.com',
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
certManager: new MemoryCertManager(),
environment: 'integration',
retryOptions: {},
} as any)).toThrow();
@ -21,7 +21,7 @@ tap.test('constructor throws without challengeHandlers', async () => {
tap.test('constructor accepts valid challengeHandlers', async () => {
const sa = new SmartAcme({
accountEmail: 'test@example.com',
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
certManager: new MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [new DummyHandler()],

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartacme',
version: '6.0.0',
version: '7.0.0',
description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
}

90
ts/certmanagers.ts Normal file
View File

@ -0,0 +1,90 @@
import * as plugins from './smartacme.plugins.js';
import type { ICertManager } from './interfaces/certmanager.js';
import { SmartacmeCert } from './smartacme.classes.cert.js';
/**
* In-memory certificate manager for mongoless mode.
* Stores certificates in memory only and does not connect to MongoDB.
*/
export class MemoryCertManager implements ICertManager {
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
private certs: Map<string, SmartacmeCert> = new Map();
constructor() {
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
public async init(): Promise<void> {
// no-op for in-memory store
}
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
return this.certs.get(domainName) ?? null;
}
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
this.certs.set(cert.domainName, cert);
const interest = this.interestMap.findInterest(cert.domainName);
if (interest) {
interest.fullfillInterest(cert);
interest.markLost();
}
}
public async deleteCertificate(domainName: string): Promise<void> {
this.certs.delete(domainName);
}
public async close(): Promise<void> {
// no-op
}
}
/**
* MongoDB-backed certificate manager using EasyStore from smartdata.
*/
export class MongoCertManager implements ICertManager {
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
private db: plugins.smartdata.SmartdataDb;
private store: plugins.smartdata.EasyStore<Record<string, any>>;
/**
* @param mongoDescriptor MongoDB connection settings
*/
constructor(mongoDescriptor: plugins.smartdata.IMongoDescriptor) {
this.db = new plugins.smartdata.SmartdataDb(mongoDescriptor);
// Use a single EasyStore document to hold all certs keyed by domainName
this.store = new plugins.smartdata.EasyStore<Record<string, any>>(
'smartacme-certs',
this.db,
);
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
public async init(): Promise<void> {
await this.db.init();
}
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
const data = await this.store.readKey(domainName);
return data ? new SmartacmeCert(data) : null;
}
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
// write plain object for persistence
await this.store.writeKey(cert.domainName, { ...cert });
const interest = this.interestMap.findInterest(cert.domainName);
if (interest) {
interest.fullfillInterest(cert);
interest.markLost();
}
}
public async deleteCertificate(domainName: string): Promise<void> {
await this.store.deleteKey(domainName);
}
public async close(): Promise<void> {
await this.db.close();
}
}

View File

@ -5,14 +5,14 @@ import type { IChallengeHandler } from './IChallengeHandler.js';
* DNS-01 challenge handler using CloudflareAccount and Smartdns.
*/
export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.IDnsChallenge> {
private cf: any;
private cf: plugins.tsclass.network.IConvenientDnsProvider;
private smartdns: plugins.smartdnsClient.Smartdns;
constructor(
cloudflareAccount: any,
convenientDnsProvider: plugins.tsclass.network.IConvenientDnsProvider,
smartdnsInstance?: plugins.smartdnsClient.Smartdns,
) {
this.cf = cloudflareAccount;
this.cf = convenientDnsProvider;
this.smartdns = smartdnsInstance ?? new plugins.smartdnsClient.Smartdns({});
}
@ -23,18 +23,14 @@ export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.I
public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
// set DNS TXT record
await this.cf.convenience.acmeSetDnsChallenge(ch);
// wait for DNS propagation
await this.smartdns.checkUntilAvailable(
ch.hostName,
'TXT',
ch.challenge,
100,
5000,
);
}
public async cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
// remove DNS TXT record
await this.cf.convenience.acmeRemoveDnsChallenge(ch);
}
public async checkWetherDomainIsSupported(domainArg: string): Promise<boolean> {
return this.cf.convenience.isDomainSupported(domainArg);
}
}

View File

@ -6,14 +6,14 @@ import type { IChallengeHandler } from './IChallengeHandler.js';
* HTTP-01 ACME challenge handler using file-system webroot.
* Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/.
*/
export interface Http01HandlerOptions {
export interface Http01WebrootOptions {
/**
* Directory that serves HTTP requests for /.well-known/acme-challenge
*/
webroot: string;
}
export class Http01Handler implements IChallengeHandler<{
export class Http01Webroot implements IChallengeHandler<{
type: string;
token: string;
keyAuthorization: string;
@ -21,7 +21,7 @@ export class Http01Handler implements IChallengeHandler<{
}> {
private webroot: string;
constructor(options: Http01HandlerOptions) {
constructor(options: Http01WebrootOptions) {
this.webroot = options.webroot;
}

View File

@ -0,0 +1,67 @@
import type { IChallengeHandler } from './IChallengeHandler.js';
/**
* HTTP-01 ACME challenge handler using in-memory storage.
* Stores challenge tokens and key authorizations in memory
* and serves them via handleRequest for arbitrary HTTP servers.
*/
export interface Http01MemoryHandlerChallenge {
type: string;
token: string;
keyAuthorization: string;
webPath: string;
}
export class Http01MemoryHandler implements IChallengeHandler<Http01MemoryHandlerChallenge> {
private store: Map<string, string> = new Map();
public getSupportedTypes(): string[] {
return ['http-01'];
}
public async prepare(ch: Http01MemoryHandlerChallenge): Promise<void> {
this.store.set(ch.token, ch.keyAuthorization);
}
public async verify(_ch: Http01MemoryHandlerChallenge): Promise<void> {
// No-op
return;
}
public async cleanup(ch: Http01MemoryHandlerChallenge): Promise<void> {
this.store.delete(ch.token);
}
/**
* HTTP request handler for serving ACME HTTP-01 challenges.
* @param req HTTP request object (should have url property)
* @param res HTTP response object
* @param next Optional next() callback for Express-style fallthrough
*/
public handleRequest(req: any, res: any, next?: () => void): void {
const url = req.url || '';
const prefix = '/.well-known/acme-challenge/';
if (!url.startsWith(prefix)) {
if (next) {
return next();
}
res.statusCode = 404;
return res.end();
}
const token = url.slice(prefix.length);
const keyAuth = this.store.get(token);
if (keyAuth !== undefined) {
if (typeof res.status === 'function' && typeof res.send === 'function') {
return res.status(200).send(keyAuth);
}
res.statusCode = 200;
res.setHeader('content-type', 'text/plain');
return res.end(keyAuth);
}
if (next) {
return next();
}
res.statusCode = 404;
return res.end();
}
}

View File

@ -1,4 +1,5 @@
export type { IChallengeHandler } from './IChallengeHandler.js';
// Removed legacy handler adapter
export { Dns01Handler } from './Dns01Handler.js';
export { Http01Handler } from './Http01Handler.js';
export { Http01Webroot } from './Http01Handler.js';
export { Http01MemoryHandler } from './Http01MemoryHandler.js';

View File

@ -1,2 +1,4 @@
export * from './smartacme.classes.smartacme.js';
export { SmartacmeCert as Cert } from './smartacme.classes.cert.js';
export type { ICertManager } from './interfaces/certmanager.js';
export { MemoryCertManager, MongoCertManager } from './certmanagers.js';

View File

@ -0,0 +1,37 @@
import type { InterestMap } from '@push.rocks/lik';
import type { SmartacmeCert } from '../smartacme.classes.cert.js';
// (ICertRecord removed; use SmartacmeCert directly)
/**
* Interface for certificate storage managers.
* Users can implement this to provide custom persistence (in-memory,
* file-based, Redis, etc.).
*/
export interface ICertManager {
/**
* Map for coordinating concurrent certificate requests.
*/
interestMap: InterestMap<string, SmartacmeCert>;
/**
* Initialize the store (e.g., connect to database).
*/
init(): Promise<void>;
/**
* Retrieve a certificate record by domain name.
* Returns null if none found.
*/
retrieveCertificate(domainName: string): Promise<SmartacmeCert | null>;
/**
* Store a certificate record. Fulfills any pending interests.
*/
storeCertificate(cert: SmartacmeCert): Promise<void>;
/**
* Delete a certificate record by domain name.
*/
deleteCertificate(domainName: string): Promise<void>;
/**
* Close the store (e.g., disconnect database).
*/
close(): Promise<void>;
}

View File

@ -1,64 +1,39 @@
import * as plugins from './smartacme.plugins.js';
import * as interfaces from './interfaces/index.js';
import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
import { Collection, svDb, unI } from '@push.rocks/smartdata';
@plugins.smartdata.Collection(() => {
return SmartacmeCertManager.activeDB;
})
export class SmartacmeCert
extends plugins.smartdata.SmartDataDbDoc<SmartacmeCert, plugins.tsclass.network.ICert>
implements plugins.tsclass.network.ICert
{
@unI()
/**
* Plain certificate record.
*/
export class SmartacmeCert {
public id: string;
@svDb()
public domainName: string;
@svDb()
public created: number;
@svDb()
public privateKey: string;
@svDb()
public publicKey: string;
@svDb()
public csr: string;
@svDb()
public validUntil: number;
constructor(data: Partial<SmartacmeCert> = {}) {
this.id = data.id || '';
this.domainName = data.domainName || '';
this.created = data.created || Date.now();
this.privateKey = data.privateKey || '';
this.publicKey = data.publicKey || '';
this.csr = data.csr || '';
this.validUntil = data.validUntil || 0;
}
/**
* Check if certificate is still valid.
*/
public isStillValid(): boolean {
return this.validUntil >= Date.now();
}
/**
* Check if certificate needs renewal (e.g., expires in <10 days).
*/
public shouldBeRenewed(): boolean {
const shouldBeValidAtLeastUntil =
Date.now() +
plugins.smarttime.getMilliSecondsFromUnits({
days: 10,
});
return !(this.validUntil >= shouldBeValidAtLeastUntil);
}
public update(certDataArg: plugins.tsclass.network.ICert) {
Object.keys(certDataArg).forEach((key) => {
this[key] = certDataArg[key];
});
}
constructor(optionsArg: plugins.tsclass.network.ICert) {
super();
if (optionsArg) {
Object.keys(optionsArg).forEach((key) => {
this[key] = optionsArg[key];
});
}
const threshold = Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 10 });
return this.validUntil < threshold;
}
}

View File

@ -1,77 +0,0 @@
import * as plugins from './smartacme.plugins.js';
import { SmartacmeCert } from './smartacme.classes.cert.js';
import { SmartAcme } from './smartacme.classes.smartacme.js';
import * as interfaces from './interfaces/index.js';
export class SmartacmeCertManager {
// =========
// STATIC
// =========
public static activeDB: plugins.smartdata.SmartdataDb;
// =========
// INSTANCE
// =========
private mongoDescriptor: plugins.smartdata.IMongoDescriptor;
public smartdataDb: plugins.smartdata.SmartdataDb;
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
constructor(
smartAcmeArg: SmartAcme,
optionsArg: {
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
},
) {
this.mongoDescriptor = optionsArg.mongoDescriptor;
}
public async init() {
// Smartdata DB
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.mongoDescriptor);
await this.smartdataDb.init();
SmartacmeCertManager.activeDB = this.smartdataDb;
// Pending Map
this.interestMap = new plugins.lik.InterestMap((certName) => certName);
}
/**
* retrieves a certificate
* @returns the Cert class or null
* @param certDomainNameArg the domain Name to retrieve the vcertificate for
*/
public async retrieveCertificate(certDomainNameArg: string): Promise<SmartacmeCert> {
const existingCertificate: SmartacmeCert = await SmartacmeCert.getInstance<SmartacmeCert>({
domainName: certDomainNameArg,
});
if (existingCertificate) {
return existingCertificate;
} else {
return null;
}
}
/**
* stores the certificate
* @param optionsArg
*/
public async storeCertificate(optionsArg: plugins.tsclass.network.ICert) {
const cert = new SmartacmeCert(optionsArg);
await cert.save();
const interest = this.interestMap.findInterest(cert.domainName);
if (interest) {
interest.fullfillInterest(cert);
interest.markLost();
}
}
public async deleteCertificate(certDomainNameArg: string) {
const cert: SmartacmeCert = await SmartacmeCert.getInstance<SmartacmeCert>({
domainName: certDomainNameArg,
});
await cert.delete();
}
}

View File

@ -1,8 +1,8 @@
import * as plugins from './smartacme.plugins.js';
import { SmartacmeCert } from './smartacme.classes.cert.js';
import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
import type { ICertManager } from './interfaces/certmanager.js';
import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
import { commitinfo } from './00_commitinfo_data.js';
import { SmartacmeCert } from './smartacme.classes.cert.js';
/**
* the options for the class @see SmartAcme
@ -10,7 +10,10 @@ import { commitinfo } from './00_commitinfo_data.js';
export interface ISmartAcmeOptions {
accountPrivateKey?: string;
accountEmail: string;
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
/**
* Certificate storage manager (e.g., Mongo or in-memory).
*/
certManager: ICertManager;
// Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
environment: 'production' | 'integration';
/**
@ -59,8 +62,8 @@ export class SmartAcme {
private privateKey: string;
// certmanager
private certmanager: SmartacmeCertManager;
// certificate manager for persistence (implements ICertManager)
private certmanager: ICertManager;
private certmatcher: SmartacmeCertMatcher;
// retry/backoff configuration (resolved with defaults)
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
@ -78,10 +81,10 @@ export class SmartAcme {
this.logger.enableConsole();
// initialize retry/backoff options
this.retryOptions = {
retries: optionsArg.retryOptions?.retries ?? 3,
factor: optionsArg.retryOptions?.factor ?? 2,
retries: optionsArg.retryOptions?.retries ?? 10,
factor: optionsArg.retryOptions?.factor ?? 4,
minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 60000,
};
// initialize challenge handlers (must provide at least one)
if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
@ -107,10 +110,11 @@ export class SmartAcme {
this.privateKey =
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
// CertMangaer
this.certmanager = new SmartacmeCertManager(this, {
mongoDescriptor: this.options.mongoDescriptor,
});
// Initialize certificate manager
if (!this.options.certManager) {
throw new Error('You must provide a certManager via options.certManager');
}
this.certmanager = this.options.certManager;
await this.certmanager.init();
// CertMatcher
@ -138,9 +142,14 @@ export class SmartAcme {
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
}
public async stop() {
await this.certmanager.smartdataDb.close();
/**
* Stops the SmartAcme instance and closes certificate store connections.
*/
public async stop() {
if (this.certmanager && typeof (this.certmanager as any).close === 'function') {
await (this.certmanager as any).close();
}
}
/** Retry helper with exponential backoff */
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
let attempt = 0;
@ -221,7 +230,8 @@ export class SmartAcme {
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
return retrievedCertificate;
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
await retrievedCertificate.delete();
// Remove old certificate via certManager
await this.certmanager.deleteCertificate(certDomainName);
}
// lets make sure others get the same interest
@ -277,15 +287,45 @@ export class SmartAcme {
}
this.pendingChallenges.push(input);
try {
// Prepare the challenge (set DNS record, write file, etc.)
await this.retry(() => handler.prepare(input), `${type}.prepare`);
if (handler.verify) {
await this.retry(() => handler.verify!(input), `${type}.verify`);
} else {
await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
// For DNS-01, wait for propagation before verification
if (type === 'dns-01') {
const dnsInput = input as { hostName: string; challenge: string };
// Wait for authoritative DNS propagation before ACME verify
await this.retry(
() => this.smartdns.checkUntilAvailable(dnsInput.hostName, 'TXT', dnsInput.challenge, 100, 5000),
`${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');
await plugins.smartdelay.delayFor(60000);
}
// Official ACME verification (ensures challenge is publicly reachable)
await this.retry(
() => this.client.verifyChallenge(authz, selectedChallengeArg),
`${type}.verifyChallenge`,
);
// Notify ACME server to complete the challenge
await this.retry(
() => this.client.completeChallenge(selectedChallengeArg),
`${type}.completeChallenge`,
);
// Wait for valid status (warnings on staging timeouts)
try {
await this.retry(
() => this.client.waitForValidStatus(selectedChallengeArg),
`${type}.waitForValidStatus`,
);
} catch (err) {
await this.logger.log(
'warn',
`Challenge ${type} did not reach valid status in time, proceeding to finalize`,
err,
);
}
await this.retry(() => this.client.completeChallenge(selectedChallengeArg), `${type}.completeChallenge`);
await this.retry(() => this.client.waitForValidStatus(selectedChallengeArg), `${type}.waitForValidStatus`);
} finally {
// Always cleanup resource
try {
await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
} catch (err) {
@ -307,19 +347,17 @@ export class SmartAcme {
/* Done */
await this.certmanager.storeCertificate({
// Store the new certificate record
const certRecord = new SmartacmeCert({
id: plugins.smartunique.shortId(),
domainName: certDomainName,
privateKey: key.toString(),
publicKey: cert.toString(),
csr: csr.toString(),
created: Date.now(),
validUntil:
Date.now() +
plugins.smarttime.getMilliSecondsFromUnits({
days: 90,
}),
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
});
await this.certmanager.storeCertificate(certRecord);
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
currentDomainInterst.fullfillInterest(newCertificate);
@ -327,7 +365,4 @@ export class SmartAcme {
return newCertificate;
}
public async getAllCertificates(): Promise<SmartacmeCert[]> {
return SmartacmeCert.getInstances({});
}
}

View File

@ -1,3 +1,8 @@
// @apiclient.xyz scope
import * as cloudflare from '@apiclient.xyz/cloudflare';
export { cloudflare };
// @apiglobal scope
import * as typedserver from '@api.global/typedserver';