Compare commits

..

No commits in common. "master" and "v6.1.3" have entirely different histories.

27 changed files with 305 additions and 846 deletions

View File

@ -1,101 +1,5 @@
# 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)
Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency
- Introduce wipe() in ICertManager to support integration testing by clearing stored certificates
- Implement wipe() in MemoryCertManager and MongoCertManager for resetting internal state
- Refactor SmartAcme constructor to consider wiping certificates in integration mode (commented out for now)
- Update integration test assertions and add console logging for domain certificate retrieval
- Upgrade @push.rocks/tapbundle from ^6.0.0 to ^6.0.1
## 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) ## 2025-04-30 - 6.1.3 - fix(Dns01Handler)
Update dependency versions and refine Dns01Handler implementation Update dependency versions and refine Dns01Handler implementation

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartacme", "name": "@push.rocks/smartacme",
"version": "7.3.3", "version": "6.1.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,15 +45,13 @@
"@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.2.0", "@tsclass/tsclass": "^9.1.0",
"acme-client": "^5.4.0" "acme-client": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
@ -61,7 +59,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.3", "@push.rocks/tapbundle": "^6.0.0",
"@types/node": "^22.15.3" "@types/node": "^22.15.3"
}, },
"files": [ "files": [

143
pnpm-lock.yaml generated
View File

@ -26,15 +26,9 @@ 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
@ -51,8 +45,8 @@ importers:
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.2.0 specifier: ^9.1.0
version: 9.2.0 version: 9.1.0
acme-client: acme-client:
specifier: ^5.4.0 specifier: ^5.4.0
version: 5.4.0 version: 5.4.0
@ -70,8 +64,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.3 specifier: ^6.0.0
version: 6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4) version: 6.0.0(@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
@ -827,8 +821,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.4.2': '@push.rocks/smartexpect@2.2.2':
resolution: {integrity: sha512-L+aS1n5rWhf/yOh5R3zPgwycYtDr5FfrDWgasy6ShhN6Zbn/z/AOPbWcF/OpeTmx0XabWB2h5d4xBcCKLl47cQ==} resolution: {integrity: sha512-s2zJlLc6Wub7P/jgKSM51kW2UjslxQwx2BXoyJVO95OgiOwarde0AuxPR0lfRA/FvHdBfTmJf4upiWtcjYMB/Q==}
'@push.rocks/smartfeed@1.0.11': '@push.rocks/smartfeed@1.0.11':
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==} resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
@ -884,9 +878,6 @@ 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==}
@ -905,9 +896,6 @@ 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==}
@ -971,8 +959,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.3': '@push.rocks/tapbundle@6.0.0':
resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==} resolution: {integrity: sha512-ARIs189TysvI8EsPAC7LH6O0WbBYI9E7XxdihwmM6LRgLvzAbp1agfO6lOjpKrAYWKjT3KdlUEihilxOBrgTYQ==}
'@push.rocks/taskbuffer@3.1.7': '@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==} resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@ -1354,8 +1342,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.2.0': '@tsclass/tsclass@9.1.0':
resolution: {integrity: sha512-A6ULEkQfYgOnCKQVQRt26O7PRzFo4PE2EoD25RAtnuFuVrNwGynYC20Vee2c8KAOyI7nQ/LaREki9KAX4AHOHQ==} resolution: {integrity: sha512-PkG1bXK/bqVtxaRHje+iJHjtcdRHLHrNTOkzqh+jv2A7mgiyNo2YBJIl4eEJLkw1X3FwEFU4vCAtsegSmJgRug==}
'@types/accepts@1.3.7': '@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@ -1414,9 +1402,6 @@ 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==}
@ -1947,10 +1932,6 @@ 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'}
@ -2019,10 +2000,6 @@ 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==}
@ -2541,10 +2518,6 @@ 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.*}
@ -2610,10 +2583,6 @@ 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==}
@ -2834,10 +2803,6 @@ 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'}
@ -2862,10 +2827,6 @@ 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'}
@ -3727,10 +3688,6 @@ 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==}
@ -4116,10 +4073,6 @@ 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'}
@ -4165,10 +4118,6 @@ 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==}
@ -4578,7 +4527,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.2.0 '@tsclass/tsclass': 9.1.0
cloudflare: 4.2.0 cloudflare: 4.2.0
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@ -5881,7 +5830,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.4.2': '@push.rocks/smartexpect@2.2.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
@ -6032,16 +5981,6 @@ 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
@ -6102,11 +6041,6 @@ 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)':
@ -6306,7 +6240,7 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
'@push.rocks/tapbundle@6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)': '@push.rocks/tapbundle@6.0.0(@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
@ -6314,7 +6248,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.4.2 '@push.rocks/smartexpect': 2.2.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)
@ -6914,7 +6848,7 @@ snapshots:
dependencies: dependencies:
type-fest: 4.40.0 type-fest: 4.40.0
'@tsclass/tsclass@9.2.0': '@tsclass/tsclass@9.1.0':
dependencies: dependencies:
type-fest: 4.40.1 type-fest: 4.40.1
@ -6984,8 +6918,6 @@ 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
@ -7606,10 +7538,6 @@ 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:
@ -7686,8 +7614,6 @@ 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: {}
@ -8259,8 +8185,6 @@ 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:
@ -8371,20 +8295,6 @@ 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: {}
@ -8621,11 +8531,6 @@ 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
@ -8646,8 +8551,6 @@ 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: {}
@ -9687,12 +9590,6 @@ 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
@ -10196,12 +10093,6 @@ 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
@ -10267,10 +10158,6 @@ 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

202
readme.md
View File

@ -4,26 +4,19 @@ A TypeScript-based ACME client with an easy yet powerful interface for LetsEncry
## Install ## Install
Using pnpm as the package manager: To install `@push.rocks/smartacme`, you can use npm or yarn. Run one of the following commands in your project directory:
```bash ```bash
pnpm add @push.rocks/smartacme npm install @push.rocks/smartacme --save
``` ```
Ensure your project is set up to use TypeScript and ECMAScript Modules (ESM). or
## Running Tests
Tests are written using `@push.rocks/tapbundle` and can be run with:
```bash ```bash
pnpm test yarn add @push.rocks/smartacme
``` ```
To run a specific test file: Make sure your project is set up to use TypeScript and supports ECMAScript Modules (ESM).
```bash
tsx test/<test-file>.ts
```
## Usage ## Usage
@ -49,31 +42,28 @@ 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, MongoCertManager } from '@push.rocks/smartacme'; import { SmartAcme } 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');
// Initialize a certificate manager (e.g., MongoDB) // Instantiate SmartAcme with one or more ACME challenge handlers
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',
certManager, mongoDescriptor: {
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: [ // pluggable ACME challenge handlers challengeHandlers: [
new Dns01Handler(cfAccount), new Dns01Handler(cfAccount),
// add more handlers as needed (e.g., Http01Webroot, Http01MemoryHandler) // you can add more handlers, e.g. Http01Handler
], ],
challengePriority: ['dns-01'], // optional challenge ordering challengePriority: ['dns-01'], // optional ordering of challenge types
}); });
``` ```
@ -101,64 +91,14 @@ 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 via a certificate manager. 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 in your configuration. Ensure your MongoDB instance is accessible and properly configured for use with SmartAcme.
```typescript ```typescript
import { MongoCertManager } from '@push.rocks/smartacme'; const mongoDescriptor = {
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
@ -170,27 +110,23 @@ 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, MongoCertManager } from '@push.rocks/smartacme'; import { SmartAcme } 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',
certManager, mongoDescriptor: {
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();
@ -202,12 +138,12 @@ 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
This module includes three out-of-the-box ACME challenge handlers: This module includes two out-of-the-box ACME challenge handlers:
- **Dns01Handler** - **Dns01Handler**
- Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation. - Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation.
@ -222,31 +158,18 @@ main().catch(console.error);
const dnsHandler = new Dns01Handler(cfAccount); const dnsHandler = new Dns01Handler(cfAccount);
``` ```
- **Http01Webroot** - **Http01Handler**
- Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup. - Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup.
- Import path: - Import path:
```typescript ```typescript
import { Http01Webroot } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js'; import { Http01Handler } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js';
``` ```
- Example: - Example:
```typescript ```typescript
const httpHandler = new Http01Webroot({ webroot: '/var/www/html' }); const httpHandler = new Http01Handler({ webroot: '/var/www/html' });
``` ```
- **Http01MemoryHandler** Both handlers implement the `IChallengeHandler<T>` interface and can be combined in the `challengeHandlers` array.
- 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 ## Creating Custom Handlers
@ -286,7 +209,7 @@ main().catch(console.error);
challengePriority: ['my-01'], challengePriority: ['my-01'],
}); });
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. 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.
## Additional Details ## Additional Details
@ -307,6 +230,8 @@ 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
@ -322,13 +247,60 @@ console.log('Certificate Domain Name:', certDomainName); // Output: example.com
### Testing ### Testing
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: 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.
```bash ```typescript
pnpm test import { tap, expect } from '@push.rocks/tapbundle';
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,3 +0,0 @@
## Plan
Move the

View File

@ -18,12 +18,4 @@ 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

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

View File

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

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, certmanagers } from '../ts/index.js'; import { SmartAcme } 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/)
@ -14,14 +14,12 @@ const mongoDbName = (await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'))!;
const mongoDbPass = (await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'))!; const mongoDbPass = (await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'))!;
const mongoDbUrl = (await testQenv.getEnvVarOnDemand('MONGODB_URL'))!; const mongoDbUrl = (await testQenv.getEnvVarOnDemand('MONGODB_URL'))!;
let smartAcmeInstance: SmartAcme; 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 }), mongoDescriptor: { mongoDbName, mongoDbPass, mongoDbUrl },
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration', environment: 'integration',
retryOptions: {}, retryOptions: {},
challengeHandlers: [new Dns01Handler(cfAccount)], challengeHandlers: [new Dns01Handler(cfAccount)],
@ -31,10 +29,6 @@ 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';

View File

@ -1,6 +1,5 @@
import { tap, expect } from '@push.rocks/tapbundle'; import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme, certmanagers } from '../ts/index.js'; import { SmartAcme } 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
@ -8,13 +7,12 @@ 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 certmanagers.MemoryCertManager(), mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
environment: 'integration', environment: 'integration',
retryOptions: {}, retryOptions: {},
} as any)).toThrow(); } as any)).toThrow();
@ -23,39 +21,12 @@ 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 certmanagers.MemoryCertManager(), mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
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.3.3', version: '6.1.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,2 +0,0 @@
export * from './memory.js';
export * from './mongo.js';

View File

@ -1,38 +0,0 @@
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();
}
}

View File

@ -1,52 +0,0 @@
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 '../plugins.js'; import * as plugins from '../smartacme.plugins.js';
import type { IChallengeHandler } from './IChallengeHandler.js'; import type { IChallengeHandler } from './IChallengeHandler.js';
/** /**
@ -23,6 +23,14 @@ export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.I
public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> { public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
// set DNS TXT record // set DNS TXT record
await this.cf.convenience.acmeSetDnsChallenge(ch); 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> { public async cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {

View File

@ -1,29 +1,27 @@
import * as plugins from '../plugins.js'; import { promises as fs } from 'fs';
import * as path from 'path';
import type { IChallengeHandler } from './IChallengeHandler.js'; import type { IChallengeHandler } from './IChallengeHandler.js';
/** /**
* HTTP-01 ACME challenge handler using file-system webroot. * HTTP-01 ACME challenge handler using file-system webroot.
* Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/. * Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/.
*/ */
export interface Http01WebrootOptions { export interface Http01HandlerOptions {
/** /**
* Directory that serves HTTP requests for /.well-known/acme-challenge * Directory that serves HTTP requests for /.well-known/acme-challenge
*/ */
webroot: string; webroot: string;
} }
export class Http01Webroot implements IChallengeHandler<{ export class Http01Handler implements IChallengeHandler<{
type: string; type: string;
token: string; token: string;
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: Http01HandlerOptions) {
this.webroot = options.webroot; this.webroot = options.webroot;
} }
@ -33,10 +31,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 = plugins.path.join(this.webroot, relWebPath); const filePath = path.join(this.webroot, relWebPath);
const dir = plugins.path.dirname(filePath); const dir = path.dirname(filePath);
await plugins.fs.promises.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
await plugins.fs.promises.writeFile(filePath, ch.keyAuthorization, 'utf8'); await fs.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> {
@ -46,24 +44,11 @@ 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 = plugins.path.join(this.webroot, relWebPath); const filePath = path.join(this.webroot, relWebPath);
try { try {
await plugins.fs.promises.unlink(filePath); await fs.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,83 +0,0 @@
import * as plugins from '../plugins.js';
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 smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
private smartdnsClient = new plugins.smartdnsClient.Smartdns({});
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();
}
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,6 +19,4 @@ 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,5 +1,4 @@
export type { IChallengeHandler } from './IChallengeHandler.js'; export type { IChallengeHandler } from './IChallengeHandler.js';
// Removed legacy handler adapter // Removed legacy handler adapter
export { Dns01Handler } from './Dns01Handler.js'; export { Dns01Handler } from './Dns01Handler.js';
export { Http01Webroot } from './Http01Handler.js'; export { Http01Handler } from './Http01Handler.js';
export { Http01MemoryHandler } from './Http01MemoryHandler.js';

View File

@ -1,11 +1,2 @@
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';
// certmanagers
import * as certmanagers from './certmanagers/index.js';
export { certmanagers };
// handlers
import * as handlers from './handlers/index.js';
export { handlers };

View File

@ -1,36 +0,0 @@
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 {
/**
* 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>;
/**
* Optional: wipe all stored certificates (e.g., for integration testing)
*/
wipe(): Promise<void>;
}

View File

@ -1,39 +1,64 @@
import * as plugins from './plugins.js'; import * as plugins from './smartacme.plugins.js';
/** import * as interfaces from './interfaces/index.js';
* Plain certificate record.
*/ import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
export class SmartacmeCert {
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()
public id: string; public id: string;
@svDb()
public domainName: string; public domainName: string;
@svDb()
public created: number; public created: number;
@svDb()
public privateKey: string; public privateKey: string;
@svDb()
public publicKey: string; public publicKey: string;
@svDb()
public csr: string; public csr: string;
@svDb()
public validUntil: number; 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 { public isStillValid(): boolean {
return this.validUntil >= Date.now(); return this.validUntil >= Date.now();
} }
/**
* Check if certificate needs renewal (e.g., expires in <10 days).
*/
public shouldBeRenewed(): boolean { public shouldBeRenewed(): boolean {
const threshold = Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 10 }); const shouldBeValidAtLeastUntil =
return this.validUntil < threshold; 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];
});
}
} }
} }

View File

@ -0,0 +1,77 @@
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,4 +1,4 @@
import * as plugins from './plugins.js'; import * as plugins from './smartacme.plugins.js';
import * as interfaces from './interfaces/index.js'; import * as interfaces from './interfaces/index.js';
/** /**
@ -10,17 +10,10 @@ 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 | undefined { public getCertificateDomainNameByDomainName(domainNameArg: string): string {
// 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,19 +1,16 @@
import * as plugins from './plugins.js'; import * as plugins from './smartacme.plugins.js';
import type { ICertManager } from './interfaces/certmanager.js'; import { SmartacmeCert } from './smartacme.classes.cert.js';
import { SmartacmeCertManager } from './smartacme.classes.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';
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 {
accountEmail: string;
accountPrivateKey?: string; accountPrivateKey?: string;
/** accountEmail: string;
* Certificate storage manager (e.g., Mongo or in-memory). mongoDescriptor: plugins.smartdata.IMongoDescriptor;
*/
certManager: ICertManager;
// Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers` // Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
environment: 'production' | 'integration'; environment: 'production' | 'integration';
/** /**
@ -62,21 +59,17 @@ export class SmartAcme {
private privateKey: string; private privateKey: string;
// certificate manager for persistence (implements ICertManager) // certmanager
public certmanager: ICertManager; private certmanager: SmartacmeCertManager;
// 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;
@ -85,10 +78,10 @@ export class SmartAcme {
this.logger.enableConsole(); this.logger.enableConsole();
// initialize retry/backoff options // initialize retry/backoff options
this.retryOptions = { this.retryOptions = {
retries: optionsArg.retryOptions?.retries ?? 10, retries: optionsArg.retryOptions?.retries ?? 3,
factor: optionsArg.retryOptions?.factor ?? 4, factor: optionsArg.retryOptions?.factor ?? 2,
minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000, minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 60000, maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
}; };
// initialize challenge handlers (must provide at least one) // initialize challenge handlers (must provide at least one)
if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) { if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
@ -102,8 +95,6 @@ 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 +107,10 @@ export class SmartAcme {
this.privateKey = this.privateKey =
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString(); this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
// Initialize certificate manager // CertMangaer
if (!this.options.certManager) { this.certmanager = new SmartacmeCertManager(this, {
throw new Error('You must provide a certManager via options.certManager'); mongoDescriptor: this.options.mongoDescriptor,
} });
this.certmanager = this.options.certManager;
await this.certmanager.init(); await this.certmanager.init();
// CertMatcher // CertMatcher
@ -148,14 +138,9 @@ export class SmartAcme {
process.on('SIGTERM', () => this.handleSignal('SIGTERM')); process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
} }
/** public async stop() {
* Stops the SmartAcme instance and closes certificate store connections. await this.certmanager.smartdataDb.close();
*/
public async stop() {
if (this.certmanager && typeof (this.certmanager as any).close === 'function') {
await (this.certmanager as any).close();
} }
}
/** Retry helper with exponential backoff */ /** Retry helper with exponential backoff */
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> { private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
let attempt = 0; let attempt = 0;
@ -223,41 +208,24 @@ 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.interestMap.checkInterest(certDomainName)) (await this.certmanager.interestMap.checkInterest(certDomainName))
) { ) {
const existingCertificateInterest = this.interestMap.findInterest(certDomainName); const existingCertificateInterest = this.certmanager.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()) {
return retrievedCertificate; return retrievedCertificate;
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) { } else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
// Remove old certificate via certManager await retrievedCertificate.delete();
await this.certmanager.deleteCertificate(certDomainName);
} }
// lets make sure others get the same interest // lets make sure others get the same interest
const currentDomainInterst = await this.interestMap.addInterest(certDomainName); const currentDomainInterst = await this.certmanager.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({
@ -309,45 +277,15 @@ export class SmartAcme {
} }
this.pendingChallenges.push(input); this.pendingChallenges.push(input);
try { try {
// Prepare the challenge (set DNS record, write file, etc.)
await this.retry(() => handler.prepare(input), `${type}.prepare`); await this.retry(() => handler.prepare(input), `${type}.prepare`);
// For DNS-01, wait for propagation before verification if (handler.verify) {
if (type === 'dns-01') { await this.retry(() => handler.verify!(input), `${type}.verify`);
const dnsInput = input as { hostName: string; challenge: string }; } else {
// Wait for authoritative DNS propagation before ACME verify await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
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 { } finally {
// Always cleanup resource
try { try {
await this.retry(() => handler.cleanup(input), `${type}.cleanup`); await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
} catch (err) { } catch (err) {
@ -369,17 +307,19 @@ export class SmartAcme {
/* Done */ /* Done */
// Store the new certificate record await this.certmanager.storeCertificate({
const certRecord = new SmartacmeCert({
id: plugins.smartunique.shortId(), id: plugins.smartunique.shortId(),
domainName: certDomainName, domainName: certDomainName,
privateKey: key.toString(), privateKey: key.toString(),
publicKey: cert.toString(), publicKey: cert.toString(),
csr: csr.toString(), csr: csr.toString(),
created: Date.now(), 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); const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
currentDomainInterst.fullfillInterest(newCertificate); currentDomainInterst.fullfillInterest(newCertificate);
@ -387,4 +327,7 @@ export class SmartAcme {
return newCertificate; return newCertificate;
} }
public async getAllCertificates(): Promise<SmartacmeCert[]> {
return SmartacmeCert.getInstances({});
}
} }

View File

@ -1,9 +1,3 @@
// 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';
@ -19,9 +13,7 @@ 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';
@ -33,9 +25,7 @@ export {
smartdata, smartdata,
smartdelay, smartdelay,
smartdnsClient, smartdnsClient,
smartfile,
smartlog, smartlog,
smartnetwork,
smartpromise, smartpromise,
smartrequest, smartrequest,
smartunique, smartunique,