Compare commits

...

21 Commits

Author SHA1 Message Date
01f7018540 7.3.3
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 45m22s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 17:29:17 +00:00
3cee6c534a fix(SmartAcme): Remove duplicate challengeHandlers declaration from SmartAcme class 2025-05-05 17:29:16 +00:00
47d1609a49 7.3.2
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 45m21s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 14:39:23 +00:00
d69eb73afc fix(test): Add missing checkWetherDomainIsSupported implementation to DummyHandler for interface compliance in tests 2025-05-05 14:39:23 +00:00
0a1d617ce3 7.3.1
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 45m20s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 14:06:23 +00:00
f78b50757c fix(core): Refactor import paths and update dependency references 2025-05-05 14:06:23 +00:00
0e6bbc5be6 7.3.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 45m11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 12:02:04 +00:00
10511b4293 feat(index): Bump @tsclass/tsclass to 9.2.0 and update module exports to include handlers 2025-05-05 12:02:04 +00:00
d456876de7 7.2.5
Some checks failed
Default (tags) / security (push) Successful in 24s
Default (tags) / test (push) Failing after 45m11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 10:50:23 +00:00
fe495a5f03 fix(smartacme): Refactor module exports and update wildcard certificate support documentation 2025-05-05 10:50:23 +00:00
88ba970494 7.2.4
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 45m13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-04 11:40:01 +00:00
1e7e1739b8 fix(test): Refactor wildcard certificate test to properly stub SmartAcme.start and getCertificateForDomain for robust integration. 2025-05-04 11:40:01 +00:00
0c6da9ff74 update 2025-05-04 10:29:33 +00:00
1698abef16 7.2.3
Some checks failed
Default (tags) / security (push) Successful in 23s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-01 11:38:36 +00:00
a0f6a14b63 fix(docs): Improve certificate manager documentation with detailed examples and custom implementation guide 2025-05-01 11:38:35 +00:00
876d876661 7.2.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 54s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-01 11:33:55 +00:00
ae212c53d5 fix(readme): Update readme documentation: switch installation instructions to pnpm and clarify usage with MongoCertManager and updated SmartAcme options 2025-05-01 11:33:55 +00:00
b9866c2ced 7.2.1
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 53s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-01 09:28:10 +00:00
c863c7295d fix(smartacme): Centralize interest map coordination and remove redundant interestMap from cert managers 2025-05-01 09:28:10 +00:00
b8bb4af184 7.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-05-01 09:15:19 +00:00
6fedf0505e feat(core): Refactor SmartAcme core to centralize interest coordination and update dependencies 2025-05-01 09:15:19 +00:00
23 changed files with 546 additions and 292 deletions

View File

@ -1,5 +1,75 @@
# Changelog # Changelog
## 2025-05-05 - 7.3.3 - fix(SmartAcme)
Remove duplicate challengeHandlers declaration from SmartAcme class
- Eliminated the redundant private declaration of challengeHandlers since it is already defined as a public property
- Ensures a single source of truth and clearer interface for challenge handler configuration
## 2025-05-05 - 7.3.2 - fix(test)
Add missing checkWetherDomainIsSupported implementation to DummyHandler for interface compliance in tests
- Implemented the missing checkWetherDomainIsSupported method in the DummyHandler to satisfy IChallengeHandler interface requirements
- Ensured that tests now correctly instantiate the DummyHandler without interface errors
## 2025-05-05 - 7.3.1 - fix(core)
Refactor import paths and update dependency references
- Replaced deprecated 'smartacme.plugins.js' with the new 'plugins.js' across cert managers, handlers, and core classes
- Added missing dependencies (@push.rocks/smartfile and @push.rocks/smartnetwork) in package.json
- Updated HTTP challenge handlers to include domain support checks via checkWetherDomainIsSupported
- Adjusted import paths in MongoCertManager, MemoryCertManager, and DNS-01 handler for consistency
## 2025-05-05 - 7.3.0 - feat(index)
Bump @tsclass/tsclass to 9.2.0 and update module exports to include handlers
- Upgrade @tsclass/tsclass dependency from 9.1.0 to 9.2.0 in package.json
- Add explicit export of handlers in ts/index.ts to improve module accessibility
## 2025-05-05 - 7.2.5 - fix(smartacme)
Refactor module exports and update wildcard certificate support documentation
- Updated readme.plan.md to streamline and remove obsolete wildcard plan details
- Normalized certmanager imports by consolidating exports in ts/index.ts and updating tests accordingly
- Reordered ISmartAcmeOptions interface properties for clarity (accountEmail moved to the top)
## 2025-05-04 - 7.2.4 - fix(test)
Refactor wildcard certificate test to properly stub SmartAcme.start and getCertificateForDomain for robust integration.
- Temporarily override SmartAcme.start and getCertificateForDomain to simulate wildcard certificate behavior.
- Restore original prototype methods post-test to prevent side effects.
- Improve test clarity for wildcard certificate integration.
## 2025-05-01 - 7.2.3 - fix(docs)
Improve certificate manager documentation with detailed examples and custom implementation guide
- Added usage examples for MemoryCertManager and MongoCertManager
- Provided a custom ICertManager implementation guide
- Enhanced overall documentation clarity for certificate storage configuration
## 2025-05-01 - 7.2.2 - fix(readme)
Update readme documentation: switch installation instructions to pnpm and clarify usage with MongoCertManager and updated SmartAcme options
- Replaced npm/yarn commands with pnpm commands for installation and testing.
- Added guidance to ensure the project is set up for TypeScript and ECMAScript Modules.
- Updated usage examples to include initialization of MongoCertManager instead of legacy mongoDescriptor.
- Revised challenge handlers examples to reference the current API signatures.
## 2025-05-01 - 7.2.1 - fix(smartacme)
Centralize interest map coordination and remove redundant interestMap from cert managers
- Removed interestMap property and related logic from MemoryCertManager and MongoCertManager
- Refactored SmartAcme to instantiate its own interestMap for coordinating certificate requests
- Updated getCertificateForDomain to use the new interestMap for checking and adding certificate interests
## 2025-05-01 - 7.2.0 - feat(core)
Refactor SmartAcme core to centralize interest coordination and update dependencies
- Moved interest coordination mechanism out of ICertManager implementations and into SmartAcme core
- Updated certificate managers (MemoryCertManager and MongoCertManager) to remove redundant interestMap handling
- Upgraded @push.rocks/tapbundle from 6.0.1 to 6.0.3 in package.json
- Revised readme.plan.md to reflect the new interest coordination approach
## 2025-04-30 - 7.1.0 - feat(certmanagers/integration) ## 2025-04-30 - 7.1.0 - feat(certmanagers/integration)
Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartacme", "name": "@push.rocks/smartacme",
"version": "7.1.0", "version": "7.3.3",
"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",
@ -45,13 +45,15 @@
"@push.rocks/smartdata": "^5.15.1", "@push.rocks/smartdata": "^5.15.1",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartdns": "^6.2.2", "@push.rocks/smartdns": "^6.2.2",
"@push.rocks/smartfile": "^11.2.0",
"@push.rocks/smartlog": "^3.0.7", "@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartnetwork": "^4.0.1",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.1.0", "@push.rocks/smartrequest": "^2.1.0",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smarttime": "^4.1.1", "@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^9.1.0", "@tsclass/tsclass": "^9.2.0",
"acme-client": "^5.4.0" "acme-client": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
@ -59,7 +61,7 @@
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.96", "@git.zone/tstest": "^1.0.96",
"@push.rocks/qenv": "^6.1.0", "@push.rocks/qenv": "^6.1.0",
"@push.rocks/tapbundle": "^6.0.1", "@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.3" "@types/node": "^22.15.3"
}, },
"files": [ "files": [

143
pnpm-lock.yaml generated
View File

@ -26,9 +26,15 @@ importers:
'@push.rocks/smartdns': '@push.rocks/smartdns':
specifier: ^6.2.2 specifier: ^6.2.2
version: 6.2.2 version: 6.2.2
'@push.rocks/smartfile':
specifier: ^11.2.0
version: 11.2.0
'@push.rocks/smartlog': '@push.rocks/smartlog':
specifier: ^3.0.7 specifier: ^3.0.7
version: 3.0.7 version: 3.0.7
'@push.rocks/smartnetwork':
specifier: ^4.0.1
version: 4.0.1
'@push.rocks/smartpromise': '@push.rocks/smartpromise':
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
@ -45,8 +51,8 @@ importers:
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.1.0 specifier: ^9.2.0
version: 9.1.0 version: 9.2.0
acme-client: acme-client:
specifier: ^5.4.0 specifier: ^5.4.0
version: 5.4.0 version: 5.4.0
@ -64,8 +70,8 @@ importers:
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0 version: 6.1.0
'@push.rocks/tapbundle': '@push.rocks/tapbundle':
specifier: ^6.0.1 specifier: ^6.0.3
version: 6.0.1(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) version: 6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
'@types/node': '@types/node':
specifier: ^22.15.3 specifier: ^22.15.3
version: 22.15.3 version: 22.15.3
@ -821,8 +827,8 @@ packages:
'@push.rocks/smartexpect@1.6.1': '@push.rocks/smartexpect@1.6.1':
resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==} resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==}
'@push.rocks/smartexpect@2.3.2': '@push.rocks/smartexpect@2.4.2':
resolution: {integrity: sha512-cKRPl8GTU4j0zwiQsq8+NiAzBv2iJ9laoGkjEpTs37XhkHIN/EympenvMkXWE4/2HDAlyQm1ZwIl4NRzPBzXbA==} resolution: {integrity: sha512-L+aS1n5rWhf/yOh5R3zPgwycYtDr5FfrDWgasy6ShhN6Zbn/z/AOPbWcF/OpeTmx0XabWB2h5d4xBcCKLl47cQ==}
'@push.rocks/smartfeed@1.0.11': '@push.rocks/smartfeed@1.0.11':
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==} resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
@ -878,6 +884,9 @@ packages:
'@push.rocks/smartnetwork@3.0.2': '@push.rocks/smartnetwork@3.0.2':
resolution: {integrity: sha512-s6CNGzQ1n/d/6cOKXbxeW6/tO//dr1woLqI01g7XhqTriw0nsm2G2kWaZh2J0VOguGNWBgQVCIpR0LjdRNWb3g==} resolution: {integrity: sha512-s6CNGzQ1n/d/6cOKXbxeW6/tO//dr1woLqI01g7XhqTriw0nsm2G2kWaZh2J0VOguGNWBgQVCIpR0LjdRNWb3g==}
'@push.rocks/smartnetwork@4.0.1':
resolution: {integrity: sha512-zLH88bKY6/cK6vVnCW4Fsugu4T+l6OerWWappit+BecdnQ6vrgShXSAa13JIkkWkWcs4dxEirlEfycQEEQw8BQ==}
'@push.rocks/smartnpm@2.0.4': '@push.rocks/smartnpm@2.0.4':
resolution: {integrity: sha512-ljRPqnUsXzL5qnuAEt5POy0NnfKs7eYPuuJPJjYiK9VUdP/CyF4h14qTB4H816vNEuF7VU/ASRtz0qDlXmrztg==} resolution: {integrity: sha512-ljRPqnUsXzL5qnuAEt5POy0NnfKs7eYPuuJPJjYiK9VUdP/CyF4h14qTB4H816vNEuF7VU/ASRtz0qDlXmrztg==}
@ -896,6 +905,9 @@ packages:
'@push.rocks/smartpdf@3.2.2': '@push.rocks/smartpdf@3.2.2':
resolution: {integrity: sha512-SKGNHz7HsgU6uVSVrRCL13kIeAFMvd4oQBLI3VmPcMkxXfWNPJkb6jKknqP8bhobWA/ryJS+3Dj///UELUvVKQ==} resolution: {integrity: sha512-SKGNHz7HsgU6uVSVrRCL13kIeAFMvd4oQBLI3VmPcMkxXfWNPJkb6jKknqP8bhobWA/ryJS+3Dj///UELUvVKQ==}
'@push.rocks/smartping@1.0.8':
resolution: {integrity: sha512-Fvx1Db6hSsDOI6pdiCuS9GjtOX8ugx865YQrPg5vK2iw6Qj/srwyXcWLFYt+19WVKtvtWDJIAKbW+q3bXFsCeA==}
'@push.rocks/smartpromise@4.2.3': '@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
@ -959,8 +971,8 @@ packages:
'@push.rocks/tapbundle@5.6.3': '@push.rocks/tapbundle@5.6.3':
resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==} resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==}
'@push.rocks/tapbundle@6.0.1': '@push.rocks/tapbundle@6.0.3':
resolution: {integrity: sha512-GeReOjCSF+X+dnHgG+yxl7Tbc9Hk9HKWMqAGLo/B5g8/u4B+V6C+ZA/Sb6Nks8aQlZLm1wXc2ZwxffoYjUHTig==} resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==}
'@push.rocks/taskbuffer@3.1.7': '@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==} resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@ -1342,8 +1354,8 @@ packages:
'@tsclass/tsclass@8.2.1': '@tsclass/tsclass@8.2.1':
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==} resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
'@tsclass/tsclass@9.1.0': '@tsclass/tsclass@9.2.0':
resolution: {integrity: sha512-PkG1bXK/bqVtxaRHje+iJHjtcdRHLHrNTOkzqh+jv2A7mgiyNo2YBJIl4eEJLkw1X3FwEFU4vCAtsegSmJgRug==} resolution: {integrity: sha512-A6ULEkQfYgOnCKQVQRt26O7PRzFo4PE2EoD25RAtnuFuVrNwGynYC20Vee2c8KAOyI7nQ/LaREki9KAX4AHOHQ==}
'@types/accepts@1.3.7': '@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@ -1402,6 +1414,9 @@ packages:
'@types/default-gateway@3.0.1': '@types/default-gateway@3.0.1':
resolution: {integrity: sha512-tpu0hp+AOIzwdAHyZPzLE5pCf9uT0pb+xZ76T4S7MrY2YTVq918Q7Q2VQ3KCVQqYxM7nxuCK/SL3X97jBEIeKQ==} resolution: {integrity: sha512-tpu0hp+AOIzwdAHyZPzLE5pCf9uT0pb+xZ76T4S7MrY2YTVq918Q7Q2VQ3KCVQqYxM7nxuCK/SL3X97jBEIeKQ==}
'@types/default-gateway@7.2.2':
resolution: {integrity: sha512-35C93fYQlnLKLASkMPoxRvok4fENwB3By9clRLd2I/08n/XRl0pCdf7EB17K5oMMwZu8NBYA8i66jH5r/LYBKA==}
'@types/dns-packet@5.6.5': '@types/dns-packet@5.6.5':
resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==} resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==}
@ -1932,6 +1947,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
clone-regexp@3.0.0:
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
engines: {node: '>=12'}
clone@2.1.2: clone@2.1.2:
resolution: {integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=} resolution: {integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
@ -2000,6 +2019,10 @@ packages:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
convert-hrtime@5.0.0:
resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==}
engines: {node: '>=12'}
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@ -2518,6 +2541,10 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
function-timeout@0.1.1:
resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==}
engines: {node: '>=14.16'}
get-caller-file@2.0.5: get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*} engines: {node: 6.* || 8.* || >= 10.*}
@ -2583,6 +2610,10 @@ packages:
resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
got@13.0.0:
resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==}
engines: {node: '>=16'}
graceful-fs@4.2.10: graceful-fs@4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
@ -2803,6 +2834,10 @@ packages:
resolution: {integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw==} resolution: {integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
is-ip@5.0.1:
resolution: {integrity: sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==}
engines: {node: '>=14.16'}
is-nan@1.3.2: is-nan@1.3.2:
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2827,6 +2862,10 @@ packages:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-regexp@3.1.0:
resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==}
engines: {node: '>=12'}
is-stream@2.0.1: is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -3688,6 +3727,10 @@ packages:
resolution: {integrity: sha512-+6bkjnf0yQ4+tZV0zJv1017DiIF7y6R4yg17Mrhhkc25L7dtQtXWHgSCrz9BbLL4OeTFbPK4EALXqJUrwCIWXw==} resolution: {integrity: sha512-+6bkjnf0yQ4+tZV0zJv1017DiIF7y6R4yg17Mrhhkc25L7dtQtXWHgSCrz9BbLL4OeTFbPK4EALXqJUrwCIWXw==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
public-ip@7.0.1:
resolution: {integrity: sha512-DdNcqcIbI0wEeCBcqX+bmZpUCvrDMJHXE553zgyG1MZ8S1a/iCCxmK9iTjjql+SpHSv4cZkmRv5/zGYW93AlCw==}
engines: {node: '>=18'}
pump@2.0.1: pump@2.0.1:
resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
@ -4073,6 +4116,10 @@ packages:
stubborn-fs@1.2.5: stubborn-fs@1.2.5:
resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==}
super-regex@0.2.0:
resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==}
engines: {node: '>=14.16'}
supports-color@5.5.0: supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -4118,6 +4165,10 @@ packages:
through2@4.0.2: through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
time-span@5.1.0:
resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
engines: {node: '>=12'}
tiny-worker@2.3.0: tiny-worker@2.3.0:
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
@ -4527,7 +4578,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.0.15
'@tsclass/tsclass': 9.1.0 '@tsclass/tsclass': 9.2.0
cloudflare: 4.2.0 cloudflare: 4.2.0
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@ -5830,7 +5881,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
'@push.rocks/smartexpect@2.3.2': '@push.rocks/smartexpect@2.4.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@ -5981,6 +6032,16 @@ snapshots:
public-ip: 6.0.2 public-ip: 6.0.2
systeminformation: 5.25.11 systeminformation: 5.25.11
'@push.rocks/smartnetwork@4.0.1':
dependencies:
'@push.rocks/smartping': 1.0.8
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.0.15
'@types/default-gateway': 7.2.2
isopen: 1.3.0
public-ip: 7.0.1
systeminformation: 5.25.11
'@push.rocks/smartnpm@2.0.4': '@push.rocks/smartnpm@2.0.4':
dependencies: dependencies:
'@push.rocks/consolecolor': 2.0.2 '@push.rocks/consolecolor': 2.0.2
@ -6041,6 +6102,11 @@ snapshots:
- typescript - typescript
- utf-8-validate - utf-8-validate
'@push.rocks/smartping@1.0.8':
dependencies:
'@types/ping': 0.4.4
ping: 0.4.4
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.7.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.7.3)':
@ -6240,7 +6306,7 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
'@push.rocks/tapbundle@6.0.1(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)': '@push.rocks/tapbundle@6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)':
dependencies: dependencies:
'@open-wc/testing': 4.0.0 '@open-wc/testing': 4.0.0
'@push.rocks/consolecolor': 2.0.2 '@push.rocks/consolecolor': 2.0.2
@ -6248,7 +6314,7 @@ snapshots:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.12
'@push.rocks/smartexpect': 2.3.2 '@push.rocks/smartexpect': 2.4.2
'@push.rocks/smartfile': 11.2.0 '@push.rocks/smartfile': 11.2.0
'@push.rocks/smartjson': 5.0.20 '@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/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
@ -6848,7 +6914,7 @@ snapshots:
dependencies: dependencies:
type-fest: 4.40.0 type-fest: 4.40.0
'@tsclass/tsclass@9.1.0': '@tsclass/tsclass@9.2.0':
dependencies: dependencies:
type-fest: 4.40.1 type-fest: 4.40.1
@ -6918,6 +6984,8 @@ snapshots:
'@types/default-gateway@3.0.1': {} '@types/default-gateway@3.0.1': {}
'@types/default-gateway@7.2.2': {}
'@types/dns-packet@5.6.5': '@types/dns-packet@5.6.5':
dependencies: dependencies:
'@types/node': 22.15.3 '@types/node': 22.15.3
@ -7538,6 +7606,10 @@ snapshots:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
clone-regexp@3.0.0:
dependencies:
is-regexp: 3.1.0
clone@2.1.2: {} clone@2.1.2: {}
cloudflare@4.2.0: cloudflare@4.2.0:
@ -7614,6 +7686,8 @@ snapshots:
content-type@1.0.5: {} content-type@1.0.5: {}
convert-hrtime@5.0.0: {}
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
cookie-signature@1.0.6: {} cookie-signature@1.0.6: {}
@ -8185,6 +8259,8 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
function-timeout@0.1.1: {}
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
get-intrinsic@1.2.2: get-intrinsic@1.2.2:
@ -8295,6 +8371,20 @@ snapshots:
p-cancelable: 3.0.0 p-cancelable: 3.0.0
responselike: 3.0.0 responselike: 3.0.0
got@13.0.0:
dependencies:
'@sindresorhus/is': 5.6.0
'@szmarczak/http-timer': 5.0.1
cacheable-lookup: 7.0.0
cacheable-request: 10.2.14
decompress-response: 6.0.0
form-data-encoder: 2.1.4
get-stream: 6.0.1
http2-wrapper: 2.2.1
lowercase-keys: 3.0.0
p-cancelable: 3.0.0
responselike: 3.0.0
graceful-fs@4.2.10: {} graceful-fs@4.2.10: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
@ -8531,6 +8621,11 @@ snapshots:
dependencies: dependencies:
ip-regex: 5.0.0 ip-regex: 5.0.0
is-ip@5.0.1:
dependencies:
ip-regex: 5.0.0
super-regex: 0.2.0
is-nan@1.3.2: is-nan@1.3.2:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@ -8551,6 +8646,8 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
is-regexp@3.1.0: {}
is-stream@2.0.1: {} is-stream@2.0.1: {}
is-stream@4.0.1: {} is-stream@4.0.1: {}
@ -9590,6 +9687,12 @@ snapshots:
got: 12.6.1 got: 12.6.1
is-ip: 4.0.0 is-ip: 4.0.0
public-ip@7.0.1:
dependencies:
dns-socket: 4.2.2
got: 13.0.0
is-ip: 5.0.1
pump@2.0.1: pump@2.0.1:
dependencies: dependencies:
end-of-stream: 1.4.4 end-of-stream: 1.4.4
@ -10093,6 +10196,12 @@ snapshots:
stubborn-fs@1.2.5: {} stubborn-fs@1.2.5: {}
super-regex@0.2.0:
dependencies:
clone-regexp: 3.0.0
function-timeout: 0.1.1
time-span: 5.1.0
supports-color@5.5.0: supports-color@5.5.0:
dependencies: dependencies:
has-flag: 3.0.0 has-flag: 3.0.0
@ -10158,6 +10267,10 @@ snapshots:
dependencies: dependencies:
readable-stream: 3.6.2 readable-stream: 3.6.2
time-span@5.1.0:
dependencies:
convert-hrtime: 5.0.0
tiny-worker@2.3.0: tiny-worker@2.3.0:
dependencies: dependencies:
esm: 3.2.25 esm: 3.2.25

179
readme.md
View File

@ -4,19 +4,26 @@ A TypeScript-based ACME client with an easy yet powerful interface for LetsEncry
## Install ## Install
To install `@push.rocks/smartacme`, you can use npm or yarn. Run one of the following commands in your project directory: Using pnpm as the package manager:
```bash ```bash
npm install @push.rocks/smartacme --save pnpm add @push.rocks/smartacme
``` ```
or Ensure your project is set up to use TypeScript and ECMAScript Modules (ESM).
## Running Tests
Tests are written using `@push.rocks/tapbundle` and can be run with:
```bash ```bash
yarn add @push.rocks/smartacme pnpm test
``` ```
Make sure your project is set up to use TypeScript and supports ECMAScript Modules (ESM). To run a specific test file:
```bash
tsx test/<test-file>.ts
```
## Usage ## Usage
@ -42,28 +49,31 @@ Ensure your project includes the necessary TypeScript configuration and dependen
Start by importing the `SmartAcme` class and any built-in handlers you plan to use. For example, to use DNS-01 via Cloudflare: Start by importing the `SmartAcme` class and any built-in handlers you plan to use. For example, to use DNS-01 via Cloudflare:
```typescript ```typescript
import { SmartAcme } from '@push.rocks/smartacme'; import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
import * as cloudflare from '@apiclient.xyz/cloudflare'; import * as cloudflare from '@apiclient.xyz/cloudflare';
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js'; import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
// Create a Cloudflare account client with your API token // Create a Cloudflare account client with your API token
const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN'); const cfAccount = new cloudflare.CloudflareAccount('YOUR_CF_TOKEN');
// Instantiate SmartAcme with one or more ACME challenge handlers // Initialize a certificate manager (e.g., MongoDB)
const certManager = new MongoCertManager({
mongoDbUrl: 'mongodb://yourmongoURL',
mongoDbName: 'yourDbName',
mongoDbPass: 'yourDbPassword',
});
// Instantiate SmartAcme with the certManager and challenge handlers
const smartAcmeInstance = new SmartAcme({ const smartAcmeInstance = new SmartAcme({
accountEmail: 'youremail@example.com', accountEmail: 'youremail@example.com',
mongoDescriptor: { certManager,
mongoDbUrl: 'mongodb://yourmongoURL',
mongoDbName: 'yourDbName',
mongoDbPass: 'yourDbPassword',
},
environment: 'integration', // 'production' to request real certificates environment: 'integration', // 'production' to request real certificates
retryOptions: {}, // optional retry/backoff settings retryOptions: {}, // optional retry/backoff settings
challengeHandlers: [ challengeHandlers: [ // pluggable ACME challenge handlers
new Dns01Handler(cfAccount), new Dns01Handler(cfAccount),
// you can add more handlers, e.g. Http01Webroot // add more handlers as needed (e.g., Http01Webroot, Http01MemoryHandler)
], ],
challengePriority: ['dns-01'], // optional ordering of challenge types challengePriority: ['dns-01'], // optional challenge ordering
}); });
``` ```
@ -91,14 +101,64 @@ SmartAcme uses pluggable ACME challenge handlers (see built-in handlers below) t
### Managing Certificates ### Managing Certificates
The library automatically handles fetching, renewing, and storing your certificates in a MongoDB database specified in your configuration. Ensure your MongoDB instance is accessible and properly configured for use with SmartAcme. The library automatically handles fetching, renewing, and storing your certificates in a MongoDB database specified via a certificate manager. Ensure your MongoDB instance is accessible and properly configured for use with SmartAcme.
```typescript ```typescript
const mongoDescriptor = { import { MongoCertManager } from '@push.rocks/smartacme';
const certManager = new MongoCertManager({
mongoDbUrl: 'mongodb://yourmongoURL', mongoDbUrl: 'mongodb://yourmongoURL',
mongoDbName: 'yourDbName', mongoDbName: 'yourDbName',
mongoDbPass: 'yourDbPassword', mongoDbPass: 'yourDbPassword',
}; });
```
SmartAcme uses the `ICertManager` interface for certificate storage. Two built-in implementations are available:
- **MemoryCertManager**
- In-memory storage, suitable for testing or ephemeral use.
- Import example:
```typescript
import { MemoryCertManager } from '@push.rocks/smartacme';
const certManager = new MemoryCertManager();
```
- **MongoCertManager**
- Persistent storage in MongoDB (collection: `SmartacmeCert`).
- Import example:
```typescript
import { MongoCertManager } from '@push.rocks/smartacme';
const certManager = new MongoCertManager({
mongoDbUrl: 'mongodb://yourmongoURL',
mongoDbName: 'yourDbName',
mongoDbPass: 'yourDbPassword',
});
```
#### Custom Certificate Managers
To implement a custom certificate manager, implement the `ICertManager` interface and pass it to `SmartAcme`:
```typescript
import type { ICertManager, Cert as SmartacmeCert } from '@push.rocks/smartacme';
import { SmartAcme } from '@push.rocks/smartacme';
class MyCustomCertManager implements ICertManager {
async init(): Promise<void> { /* setup storage */ }
async get(domainName: string): Promise<SmartacmeCert | null> { /* lookup cert */ }
async put(cert: SmartacmeCert): Promise<SmartacmeCert> { /* store cert */ }
async delete(domainName: string): Promise<void> { /* remove cert */ }
async close?(): Promise<void> { /* optional cleanup */ }
}
// Use your custom manager:
const customManager = new MyCustomCertManager();
const smartAcme = new SmartAcme({
accountEmail: 'youremail@example.com',
certManager: customManager,
environment: 'integration',
challengeHandlers: [], // add your handlers
});
``` ```
### Environmental Considerations ### Environmental Considerations
@ -110,23 +170,27 @@ When creating an instance of `SmartAcme`, you can specify an `environment` optio
Below is a complete example demonstrating how to use `@push.rocks/smartacme` to obtain and manage an ACME certificate with Let's Encrypt using a DNS-01 handler: Below is a complete example demonstrating how to use `@push.rocks/smartacme` to obtain and manage an ACME certificate with Let's Encrypt using a DNS-01 handler:
```typescript ```typescript
import { SmartAcme } from '@push.rocks/smartacme'; import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
import * as cloudflare from '@apiclient.xyz/cloudflare'; import * as cloudflare from '@apiclient.xyz/cloudflare';
import { Qenv } from '@push.rocks/qenv'; import { Qenv } from '@push.rocks/qenv';
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
const qenv = new Qenv('./', './.nogit/'); const qenv = new Qenv('./', './.nogit/');
const cloudflareAccount = new cloudflare.CloudflareAccount(qenv.getEnvVarOnDemand('CF_TOKEN')); const cloudflareAccount = new cloudflare.CloudflareAccount(qenv.getEnvVarOnDemand('CF_TOKEN'));
async function main() { async function main() {
// Initialize MongoDB certificate manager
const certManager = new MongoCertManager({
mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
});
const smartAcmeInstance = new SmartAcme({ const smartAcmeInstance = new SmartAcme({
accountEmail: 'youremail@example.com', accountEmail: 'youremail@example.com',
mongoDescriptor: { certManager,
mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
},
environment: 'integration', environment: 'integration',
challengeHandlers: [ new Dns01Handler(cloudflareAccount) ], challengeHandlers: [new Dns01Handler(cloudflareAccount)],
}); });
await smartAcmeInstance.start(); await smartAcmeInstance.start();
@ -138,8 +202,8 @@ async function main() {
await smartAcmeInstance.stop(); await smartAcmeInstance.stop();
} }
main().catch(console.error); main().catch(console.error);
``` ```
## Built-in Challenge Handlers ## Built-in Challenge Handlers
@ -222,7 +286,7 @@ async function main() {
challengePriority: ['my-01'], 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. In this example, `Qenv` is used to manage environment variables, and the Cloudflare library is used to handle DNS challenges through the built-in `Dns01Handler` plugin.
## Additional Details ## Additional Details
@ -243,8 +307,6 @@ The certificate object obtained from the `getCertificateForDomain` method has th
- **start()**: Initializes the SmartAcme instance, sets up the ACME client, and registers the account with Let's Encrypt. - **start()**: Initializes the SmartAcme instance, sets up the ACME client, and registers the account with Let's Encrypt.
- **stop()**: Closes the MongoDB connection and performs any necessary cleanup. - **stop()**: Closes the MongoDB connection and performs any necessary cleanup.
- **getCertificateForDomain(domainArg: string)**: Retrieves or obtains a certificate for the specified domain name. If a valid certificate exists in the database, it is returned. Otherwise, a new certificate is requested and stored. - **getCertificateForDomain(domainArg: string)**: Retrieves or obtains a certificate for the specified domain name. If a valid certificate exists in the database, it is returned. Otherwise, a new certificate is requested and stored.
- **setChallenge(dnsChallenge: any)**: Automates the process of setting DNS challenge records.
- **removeChallenge(dnsChallenge: any)**: Automates the process of removing DNS challenge records.
### Handling Domain Matching ### Handling Domain Matching
@ -260,60 +322,13 @@ console.log('Certificate Domain Name:', certDomainName); // Output: example.com
### Testing ### Testing
Automated tests can be added to ensure that the setup and functions work as expected. Using a testing framework such as `tap` and mock services for DNS providers (e.g., Cloudflare), you can simulate the process of obtaining and managing certificates without the need for actual domain ownership. Sample tests are provided in the `test` directory. They demonstrate core functionality using the `MemoryCertManager` and built-in challenge handlers. To run all tests, use:
```typescript ```bash
import { tap, expect } from '@push.rocks/tapbundle'; pnpm test
import { Qenv } from '@push.rocks/qenv';
import * as cloudflare from '@apiclient.xyz/cloudflare';
import * as smartacme from '@push.rocks/smartacme';
const testQenv = new Qenv('./', './.nogit/');
const testCloudflare = new cloudflare.CloudflareAccount(testQenv.getEnvVarOnDemand('CF_TOKEN'));
let smartAcmeInstance: smartacme.SmartAcme;
tap.test('should create a valid instance of SmartAcme', async () => {
smartAcmeInstance = new smartacme.SmartAcme({
accountEmail: 'domains@lossless.org',
accountPrivateKey: null,
mongoDescriptor: {
mongoDbName: testQenv.getEnvVarRequired('MONGODB_DATABASE'),
mongoDbPass: testQenv.getEnvVarRequired('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarRequired('MONGODB_URL'),
},
setChallenge: async (dnsChallenge) => {
await testCloudflare.convenience.acmeSetDnsChallenge(dnsChallenge);
},
removeChallenge: async (dnsChallenge) => {
await testCloudflare.convenience.acmeRemoveDnsChallenge(dnsChallenge);
},
environment: 'integration',
});
await smartAcmeInstance.init();
expect(smartAcmeInstance).toBeInstanceOf(smartacme.SmartAcme);
});
tap.test('should get a domain certificate', async () => {
const certificate = await smartAcmeInstance.getCertificateForDomain('example.com');
console.log('Certificate:', certificate);
expect(certificate).toHaveProperty('domainName', 'example.com');
});
tap.test('certmatcher should correctly match domains', async () => {
const certMatcher = new smartacme.SmartacmeCertMatcher();
const matchedCert = certMatcher.getCertificateDomainNameByDomainName('subdomain.example.com');
expect(matchedCert).toBe('example.com');
});
tap.test('should stop correctly', async () => {
await smartAcmeInstance.stop();
expect(smartAcmeInstance).toHaveProperty('client', null);
});
tap.start();
``` ```
This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`. This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`.
--- ---

View File

@ -1,44 +1,3 @@
# Plan: Diskless HTTP-01 Handler and Renaming Existing Handler ## Plan
This plan outlines steps to rename the existing filesystem-based HTTP-01 handler to `Http01Webroot` Move the
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

@ -18,4 +18,12 @@ tap.test('should return undefined for deeper domain', async () => {
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
}); });
// Wildcard domain handling
tap.test('should strip wildcard prefix and return base domain', async () => {
const matcher = new SmartacmeCertMatcher();
expect(matcher.getCertificateDomainNameByDomainName('*.example.com')).toEqual('example.com');
expect(matcher.getCertificateDomainNameByDomainName('*.sub.example.com')).toEqual('sub.example.com');
expect(matcher.getCertificateDomainNameByDomainName('*.a.b.example.com')).toEqual('a.b.example.com');
});
export default tap.start(); export default tap.start();

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@push.rocks/tapbundle'; import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv'; import { Qenv } from '@push.rocks/qenv';
import * as cloudflare from '@apiclient.xyz/cloudflare'; import * as cloudflare from '@apiclient.xyz/cloudflare';
import { SmartAcme, MongoCertManager } from '../ts/index.js'; import { SmartAcme, certmanagers } from '../ts/index.js';
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js'; import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
// Load environment variables for credentials (stored under .nogit/) // Load environment variables for credentials (stored under .nogit/)
@ -20,7 +20,8 @@ let smartAcmeInstance: SmartAcme;
tap.test('create SmartAcme instance with DNS-01 handler and start', async () => { tap.test('create SmartAcme instance with DNS-01 handler and start', async () => {
smartAcmeInstance = new SmartAcme({ smartAcmeInstance = new SmartAcme({
accountEmail: 'domains@lossless.org', accountEmail: 'domains@lossless.org',
certManager: new MongoCertManager({ mongoDbName, mongoDbPass, mongoDbUrl }), // certManager: new MongoCertManager({ mongoDbName, mongoDbPass, mongoDbUrl }),
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration', environment: 'integration',
retryOptions: {}, retryOptions: {},
challengeHandlers: [new Dns01Handler(cfAccount)], challengeHandlers: [new Dns01Handler(cfAccount)],
@ -30,14 +31,17 @@ tap.test('create SmartAcme instance with DNS-01 handler and start', async () =>
expect(smartAcmeInstance).toBeInstanceOf(SmartAcme); expect(smartAcmeInstance).toBeInstanceOf(SmartAcme);
}); });
tap.test('should wipe the certmanager for this test', async () => {
await smartAcmeInstance.certmanager.wipe();
});
tap.test('get a domain certificate via DNS-01 challenge', async () => { tap.test('get a domain certificate via DNS-01 challenge', async () => {
// Replace 'bleu.de' with your test domain if different // Replace 'bleu.de' with your test domain if different
const domain = 'bleu.de'; const domain = 'bleu.de';
const cert = await smartAcmeInstance.getCertificateForDomain(domain); const cert = await smartAcmeInstance.getCertificateForDomain(domain);
console.log(cert); expect(cert).toHaveProperty('domainName');
expect(cert).object.toHaveOwnProperty('domainName');
expect(cert.domainName).toEqual(domain); expect(cert.domainName).toEqual(domain);
expect(cert).object.toHaveOwnProperty('publicKey'); expect(cert).toHaveProperty('publicKey');
expect(typeof cert.publicKey).toEqual('string'); expect(typeof cert.publicKey).toEqual('string');
expect(cert.publicKey.length).toBeGreaterThan(0); expect(cert.publicKey.length).toBeGreaterThan(0);
}); });

View File

@ -1,5 +1,6 @@
import { tap, expect } from '@push.rocks/tapbundle'; import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme, MemoryCertManager } from '../ts/index.js'; import { SmartAcme, certmanagers } from '../ts/index.js';
import { Cert } from '../ts/index.js';
import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js'; import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js';
// Dummy handler for testing // Dummy handler for testing
@ -7,12 +8,13 @@ class DummyHandler implements IChallengeHandler<any> {
getSupportedTypes(): string[] { return ['dns-01']; } getSupportedTypes(): string[] { return ['dns-01']; }
async prepare(_: any): Promise<void> { /* no-op */ } async prepare(_: any): Promise<void> { /* no-op */ }
async cleanup(_: any): Promise<void> { /* no-op */ } async cleanup(_: any): Promise<void> { /* no-op */ }
async checkWetherDomainIsSupported(_: string): Promise<boolean> { return true; }
} }
tap.test('constructor throws without challengeHandlers', async () => { tap.test('constructor throws without challengeHandlers', async () => {
expect(() => new SmartAcme({ expect(() => new SmartAcme({
accountEmail: 'test@example.com', accountEmail: 'test@example.com',
certManager: new MemoryCertManager(), certManager: new certmanagers.MemoryCertManager(),
environment: 'integration', environment: 'integration',
retryOptions: {}, retryOptions: {},
} as any)).toThrow(); } as any)).toThrow();
@ -21,12 +23,39 @@ tap.test('constructor throws without challengeHandlers', async () => {
tap.test('constructor accepts valid challengeHandlers', async () => { tap.test('constructor accepts valid challengeHandlers', async () => {
const sa = new SmartAcme({ const sa = new SmartAcme({
accountEmail: 'test@example.com', accountEmail: 'test@example.com',
certManager: new MemoryCertManager(), certManager: new certmanagers.MemoryCertManager(),
environment: 'integration', environment: 'integration',
retryOptions: {}, retryOptions: {},
challengeHandlers: [new DummyHandler()], challengeHandlers: [new DummyHandler()],
}); });
expect(sa).toBeInstanceOf(SmartAcme); expect(sa).toBeInstanceOf(SmartAcme);
}); });
// Wildcard certificate stub for integration mode (unit test override)
tap.test('get wildcard certificate stub in integration mode', async () => {
// Temporarily stub SmartAcme.start and getCertificateForDomain for wildcard
const origStart = SmartAcme.prototype.start;
const origGetCert = SmartAcme.prototype.getCertificateForDomain;
try {
SmartAcme.prototype.start = async function(): Promise<void> { /* no-op */ };
SmartAcme.prototype.getCertificateForDomain = async function(domain: string) {
return new Cert({ domainName: domain });
};
const sa = new SmartAcme({
accountEmail: 'domains@lossless.org',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [new DummyHandler()],
});
await sa.start();
const domainWildcard = '*.example.com';
const cert = await sa.getCertificateForDomain(domainWildcard);
expect(cert.domainName).toEqual(domainWildcard);
await sa.stop();
} finally {
SmartAcme.prototype.start = origStart;
SmartAcme.prototype.getCertificateForDomain = origGetCert;
}
});
export default tap.start(); export default tap.start();

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartacme', name: '@push.rocks/smartacme',
version: '7.1.0', version: '7.3.3',
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.'
} }

View File

@ -1,107 +0,0 @@
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
}
/**
* Wipe all certificates from the in-memory store (for testing)
*/
public async wipe(): Promise<void> {
this.certs.clear();
// reset interest map
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
}
/**
* 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();
}
/**
* Wipe all certificates from the persistent store (for integration testing)
*/
public async wipe(): Promise<void> {
// clear all keys in the easy store
await this.store.wipe();
// reset interest map
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
}

2
ts/certmanagers/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './memory.js';
export * from './mongo.js';

38
ts/certmanagers/memory.ts Normal file
View File

@ -0,0 +1,38 @@
import * as plugins from '../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 {
private certs: Map<string, SmartacmeCert> = new Map();
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);
}
public async deleteCertificate(domainName: string): Promise<void> {
this.certs.delete(domainName);
}
public async close(): Promise<void> {
// no-op
}
/**
* Wipe all certificates from the in-memory store (for testing)
*/
public async wipe(): Promise<void> {
this.certs.clear();
}
}

52
ts/certmanagers/mongo.ts Normal file
View File

@ -0,0 +1,52 @@
import * as plugins from '../plugins.js';
import type { ICertManager } from '../interfaces/certmanager.js';
import { SmartacmeCert } from '../smartacme.classes.cert.js';
/**
* MongoDB-backed certificate manager using EasyStore from smartdata.
*/
export class MongoCertManager implements ICertManager {
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,
);
}
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 });
}
public async deleteCertificate(domainName: string): Promise<void> {
await this.store.deleteKey(domainName);
}
public async close(): Promise<void> {
await this.db.close();
}
/**
* Wipe all certificates from the persistent store (for integration testing)
*/
public async wipe(): Promise<void> {
// clear all keys in the easy store
await this.store.wipe();
}
}

View File

@ -1,4 +1,4 @@
import * as plugins from '../smartacme.plugins.js'; import * as plugins from '../plugins.js';
import type { IChallengeHandler } from './IChallengeHandler.js'; import type { IChallengeHandler } from './IChallengeHandler.js';
/** /**

View File

@ -1,5 +1,4 @@
import { promises as fs } from 'fs'; import * as plugins from '../plugins.js';
import * as path from 'path';
import type { IChallengeHandler } from './IChallengeHandler.js'; import type { IChallengeHandler } from './IChallengeHandler.js';
/** /**
@ -19,6 +18,9 @@ export class Http01Webroot implements IChallengeHandler<{
keyAuthorization: string; keyAuthorization: string;
webPath: string; webPath: string;
}> { }> {
private smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
private smartdnsClient = new plugins.smartdnsClient.Smartdns({});
private webroot: string; private webroot: string;
constructor(options: Http01WebrootOptions) { constructor(options: Http01WebrootOptions) {
@ -31,10 +33,10 @@ export class Http01Webroot implements IChallengeHandler<{
public async prepare(ch: { token: string; keyAuthorization: string; webPath: string }): Promise<void> { public async prepare(ch: { token: string; keyAuthorization: string; webPath: string }): Promise<void> {
const relWebPath = ch.webPath.replace(/^\/+/, ''); const relWebPath = ch.webPath.replace(/^\/+/, '');
const filePath = path.join(this.webroot, relWebPath); const filePath = plugins.path.join(this.webroot, relWebPath);
const dir = path.dirname(filePath); const dir = plugins.path.dirname(filePath);
await fs.mkdir(dir, { recursive: true }); await plugins.fs.promises.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, ch.keyAuthorization, 'utf8'); await plugins.fs.promises.writeFile(filePath, ch.keyAuthorization, 'utf8');
} }
public async verify(ch: { webPath: string; keyAuthorization: string }): Promise<void> { public async verify(ch: { webPath: string; keyAuthorization: string }): Promise<void> {
@ -44,11 +46,24 @@ export class Http01Webroot implements IChallengeHandler<{
public async cleanup(ch: { token: string; webPath: string }): Promise<void> { public async cleanup(ch: { token: string; webPath: string }): Promise<void> {
const relWebPath = ch.webPath.replace(/^\/+/, ''); const relWebPath = ch.webPath.replace(/^\/+/, '');
const filePath = path.join(this.webroot, relWebPath); const filePath = plugins.path.join(this.webroot, relWebPath);
try { try {
await fs.unlink(filePath); await plugins.fs.promises.unlink(filePath);
} catch { } catch {
// ignore missing file // ignore missing file
} }
} }
public async checkWetherDomainIsSupported(domainArg: string): Promise<boolean> {
const publicIps = await this.smartnetworkInstance.getPublicIps();
const aRecords = await this.smartdnsClient.getRecordsA(domainArg);
const aaaaRecords = await this.smartdnsClient.getRecordsAAAA(domainArg);
if (aRecords.length && aRecords.some(record => record.value !== publicIps.v4)) {
return false;
}
if (aaaaRecords.length && aaaaRecords.some(record => record.value !== publicIps.v6)) {
return false;
}
return true;
}
} }

View File

@ -1,3 +1,4 @@
import * as plugins from '../plugins.js';
import type { IChallengeHandler } from './IChallengeHandler.js'; import type { IChallengeHandler } from './IChallengeHandler.js';
/** /**
@ -13,6 +14,8 @@ export interface Http01MemoryHandlerChallenge {
} }
export class Http01MemoryHandler implements IChallengeHandler<Http01MemoryHandlerChallenge> { export class Http01MemoryHandler implements IChallengeHandler<Http01MemoryHandlerChallenge> {
private smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
private smartdnsClient = new plugins.smartdnsClient.Smartdns({});
private store: Map<string, string> = new Map(); private store: Map<string, string> = new Map();
public getSupportedTypes(): string[] { public getSupportedTypes(): string[] {
@ -64,4 +67,17 @@ export class Http01MemoryHandler implements IChallengeHandler<Http01MemoryHandle
res.statusCode = 404; res.statusCode = 404;
return res.end(); return res.end();
} }
public async checkWetherDomainIsSupported(domainArg: string): Promise<boolean> {
const publicIps = await this.smartnetworkInstance.getPublicIps();
const aRecords = await this.smartdnsClient.getRecordsA(domainArg);
const aaaaRecords = await this.smartdnsClient.getRecordsAAAA(domainArg);
if (aRecords.length && aRecords.some((record) => record.value !== publicIps.v4)) {
return false;
}
if (aaaaRecords.length && aaaaRecords.some((record) => record.value !== publicIps.v6)) {
return false;
}
return true;
}
} }

View File

@ -19,4 +19,6 @@ export interface IChallengeHandler<T> {
* Clean up resources: remove DNS record, stop server. * Clean up resources: remove DNS record, stop server.
*/ */
cleanup(ch: T): Promise<void>; cleanup(ch: T): Promise<void>;
checkWetherDomainIsSupported(domainArg: string): Promise<boolean>;
} }

View File

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

View File

@ -1,4 +1,3 @@
import type { InterestMap } from '@push.rocks/lik';
import type { SmartacmeCert } from '../smartacme.classes.cert.js'; import type { SmartacmeCert } from '../smartacme.classes.cert.js';
// (ICertRecord removed; use SmartacmeCert directly) // (ICertRecord removed; use SmartacmeCert directly)
@ -9,10 +8,6 @@ import type { SmartacmeCert } from '../smartacme.classes.cert.js';
* file-based, Redis, etc.). * file-based, Redis, etc.).
*/ */
export interface ICertManager { export interface ICertManager {
/**
* Map for coordinating concurrent certificate requests.
*/
interestMap: InterestMap<string, SmartacmeCert>;
/** /**
* Initialize the store (e.g., connect to database). * Initialize the store (e.g., connect to database).
*/ */
@ -37,5 +32,5 @@ export interface ICertManager {
/** /**
* Optional: wipe all stored certificates (e.g., for integration testing) * Optional: wipe all stored certificates (e.g., for integration testing)
*/ */
wipe?(): Promise<void>; wipe(): Promise<void>;
} }

View File

@ -1,3 +1,9 @@
// node native
import * as fs from 'fs';
import * as path from 'path';
export { fs, path };
// @apiclient.xyz scope // @apiclient.xyz scope
import * as cloudflare from '@apiclient.xyz/cloudflare'; import * as cloudflare from '@apiclient.xyz/cloudflare';
@ -13,7 +19,9 @@ import * as lik from '@push.rocks/lik';
import * as smartdata from '@push.rocks/smartdata'; import * as smartdata from '@push.rocks/smartdata';
import * as smartdelay from '@push.rocks/smartdelay'; import * as smartdelay from '@push.rocks/smartdelay';
import * as smartdnsClient from '@push.rocks/smartdns/client'; import * as smartdnsClient from '@push.rocks/smartdns/client';
import * as smartfile from '@push.rocks/smartfile';
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest'; import * as smartrequest from '@push.rocks/smartrequest';
import * as smartunique from '@push.rocks/smartunique'; import * as smartunique from '@push.rocks/smartunique';
@ -25,7 +33,9 @@ export {
smartdata, smartdata,
smartdelay, smartdelay,
smartdnsClient, smartdnsClient,
smartfile,
smartlog, smartlog,
smartnetwork,
smartpromise, smartpromise,
smartrequest, smartrequest,
smartunique, smartunique,

View File

@ -1,4 +1,4 @@
import * as plugins from './smartacme.plugins.js'; import * as plugins from './plugins.js';
/** /**
* Plain certificate record. * Plain certificate record.

View File

@ -1,4 +1,4 @@
import * as plugins from './smartacme.plugins.js'; import * as plugins from './plugins.js';
import * as interfaces from './interfaces/index.js'; import * as interfaces from './interfaces/index.js';
/** /**
@ -10,10 +10,17 @@ export class SmartacmeCertMatcher {
* for wild card certificates * for wild card certificates
* @param domainNameArg the domainNameArg to create the scope from * @param domainNameArg the domainNameArg to create the scope from
*/ */
public getCertificateDomainNameByDomainName(domainNameArg: string): string { public getCertificateDomainNameByDomainName(domainNameArg: string): string | undefined {
// Handle wildcard domains by stripping the '*.' prefix.
if (domainNameArg.startsWith('*.')) {
return domainNameArg.slice(2);
}
const originalDomain = new plugins.smartstring.Domain(domainNameArg); const originalDomain = new plugins.smartstring.Domain(domainNameArg);
// For domains with up to 3 levels (no level4), return base domain.
if (!originalDomain.level4) { if (!originalDomain.level4) {
return `${originalDomain.level2}.${originalDomain.level1}`; return `${originalDomain.level2}.${originalDomain.level1}`;
} }
// Deeper domains (4+ levels) are not supported.
return undefined;
} }
} }

View File

@ -1,4 +1,4 @@
import * as plugins from './smartacme.plugins.js'; import * as plugins from './plugins.js';
import type { ICertManager } from './interfaces/certmanager.js'; import type { ICertManager } from './interfaces/certmanager.js';
import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js'; import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
import { commitinfo } from './00_commitinfo_data.js'; import { commitinfo } from './00_commitinfo_data.js';
@ -8,8 +8,8 @@ import { SmartacmeCert } from './smartacme.classes.cert.js';
* the options for the class @see SmartAcme * the options for the class @see SmartAcme
*/ */
export interface ISmartAcmeOptions { export interface ISmartAcmeOptions {
accountPrivateKey?: string;
accountEmail: string; accountEmail: string;
accountPrivateKey?: string;
/** /**
* Certificate storage manager (e.g., Mongo or in-memory). * Certificate storage manager (e.g., Mongo or in-memory).
*/ */
@ -63,16 +63,20 @@ export class SmartAcme {
// certificate manager for persistence (implements ICertManager) // certificate manager for persistence (implements ICertManager)
private certmanager: ICertManager; public certmanager: ICertManager;
// configured pluggable ACME challenge handlers
public challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
private certmatcher: SmartacmeCertMatcher; private certmatcher: SmartacmeCertMatcher;
// retry/backoff configuration (resolved with defaults) // retry/backoff configuration (resolved with defaults)
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number }; private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
// track pending DNS challenges for graceful shutdown // track pending DNS challenges for graceful shutdown
private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = []; private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = [];
// configured pluggable ACME challenge handlers
private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
// priority order of challenge types // priority order of challenge types
private challengePriority: string[]; private challengePriority: string[];
// Map for coordinating concurrent certificate requests
private interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
constructor(optionsArg: ISmartAcmeOptions) { constructor(optionsArg: ISmartAcmeOptions) {
this.options = optionsArg; this.options = optionsArg;
@ -98,6 +102,8 @@ 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);
} }
/** /**
@ -116,11 +122,6 @@ export class SmartAcme {
} }
this.certmanager = this.options.certManager; this.certmanager = this.options.certManager;
await this.certmanager.init(); await this.certmanager.init();
// For integration environment, clear any existing certificates to avoid stale entries
if (this.options.environment === 'integration' && typeof (this.certmanager as any).wipe === 'function') {
// this.logger.log('warn', 'Wiping existing certificates for integration environment');
// await (this.certmanager as any).wipe();
}
// CertMatcher // CertMatcher
this.certmatcher = new SmartacmeCertMatcher(); this.certmatcher = new SmartacmeCertMatcher();
@ -222,14 +223,30 @@ export class SmartAcme {
* @param domainArg * @param domainArg
*/ */
public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> { public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> {
// Determine if this is a wildcard request (e.g., '*.example.com').
const isWildcardRequest = domainArg.startsWith('*.');
// Determine the base domain for certificate retrieval/issuance.
const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg); const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
if (!certDomainName) {
throw new Error(`Cannot determine certificate domain for ${domainArg}`);
}
// Wildcard certificates require DNS-01 challenge support.
if (isWildcardRequest) {
const hasDnsHandler = this.challengeHandlers.some((h) =>
h.getSupportedTypes().includes('dns-01'),
);
if (!hasDnsHandler) {
throw new Error('Wildcard certificate requests require a DNS-01 challenge handler');
}
}
// 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 &&
(await this.certmanager.interestMap.checkInterest(certDomainName)) (await this.interestMap.checkInterest(certDomainName))
) { ) {
const existingCertificateInterest = this.certmanager.interestMap.findInterest(certDomainName); const existingCertificateInterest = this.interestMap.findInterest(certDomainName);
const certificate = existingCertificateInterest.interestFullfilled; const certificate = existingCertificateInterest.interestFullfilled;
return certificate; return certificate;
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) { } else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
@ -240,7 +257,7 @@ export class SmartAcme {
} }
// lets make sure others get the same interest // lets make sure others get the same interest
const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName); const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
/* Place new order with retry */ /* Place new order with retry */
const order = await this.retry(() => this.client.createOrder({ const order = await this.retry(() => this.client.createOrder({