Compare commits

...

19 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
20 changed files with 430 additions and 154 deletions

View File

@ -1,5 +1,67 @@
# 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

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartacme",
"version": "7.2.0",
"version": "7.3.3",
"private": false,
"description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
"main": "dist_ts/index.js",
@ -45,13 +45,15 @@
"@push.rocks/smartdata": "^5.15.1",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartdns": "^6.2.2",
"@push.rocks/smartfile": "^11.2.0",
"@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartnetwork": "^4.0.1",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.1.0",
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^9.1.0",
"@tsclass/tsclass": "^9.2.0",
"acme-client": "^5.4.0"
},
"devDependencies": {

125
pnpm-lock.yaml generated
View File

@ -26,9 +26,15 @@ importers:
'@push.rocks/smartdns':
specifier: ^6.2.2
version: 6.2.2
'@push.rocks/smartfile':
specifier: ^11.2.0
version: 11.2.0
'@push.rocks/smartlog':
specifier: ^3.0.7
version: 3.0.7
'@push.rocks/smartnetwork':
specifier: ^4.0.1
version: 4.0.1
'@push.rocks/smartpromise':
specifier: ^4.2.3
version: 4.2.3
@ -45,8 +51,8 @@ importers:
specifier: ^3.0.9
version: 3.0.9
'@tsclass/tsclass':
specifier: ^9.1.0
version: 9.1.0
specifier: ^9.2.0
version: 9.2.0
acme-client:
specifier: ^5.4.0
version: 5.4.0
@ -878,6 +884,9 @@ packages:
'@push.rocks/smartnetwork@3.0.2':
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':
resolution: {integrity: sha512-ljRPqnUsXzL5qnuAEt5POy0NnfKs7eYPuuJPJjYiK9VUdP/CyF4h14qTB4H816vNEuF7VU/ASRtz0qDlXmrztg==}
@ -896,6 +905,9 @@ packages:
'@push.rocks/smartpdf@3.2.2':
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':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
@ -1342,8 +1354,8 @@ packages:
'@tsclass/tsclass@8.2.1':
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
'@tsclass/tsclass@9.1.0':
resolution: {integrity: sha512-PkG1bXK/bqVtxaRHje+iJHjtcdRHLHrNTOkzqh+jv2A7mgiyNo2YBJIl4eEJLkw1X3FwEFU4vCAtsegSmJgRug==}
'@tsclass/tsclass@9.2.0':
resolution: {integrity: sha512-A6ULEkQfYgOnCKQVQRt26O7PRzFo4PE2EoD25RAtnuFuVrNwGynYC20Vee2c8KAOyI7nQ/LaREki9KAX4AHOHQ==}
'@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@ -1402,6 +1414,9 @@ packages:
'@types/default-gateway@3.0.1':
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':
resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==}
@ -1932,6 +1947,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clone-regexp@3.0.0:
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
engines: {node: '>=12'}
clone@2.1.2:
resolution: {integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=}
engines: {node: '>=0.8'}
@ -2000,6 +2019,10 @@ packages:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
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:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@ -2518,6 +2541,10 @@ packages:
function-bind@1.1.2:
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:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@ -2583,6 +2610,10 @@ packages:
resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==}
engines: {node: '>=14.16'}
got@13.0.0:
resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==}
engines: {node: '>=16'}
graceful-fs@4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
@ -2803,6 +2834,10 @@ packages:
resolution: {integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw==}
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:
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
engines: {node: '>= 0.4'}
@ -2827,6 +2862,10 @@ packages:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
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:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
@ -3688,6 +3727,10 @@ packages:
resolution: {integrity: sha512-+6bkjnf0yQ4+tZV0zJv1017DiIF7y6R4yg17Mrhhkc25L7dtQtXWHgSCrz9BbLL4OeTFbPK4EALXqJUrwCIWXw==}
engines: {node: '>=14.16'}
public-ip@7.0.1:
resolution: {integrity: sha512-DdNcqcIbI0wEeCBcqX+bmZpUCvrDMJHXE553zgyG1MZ8S1a/iCCxmK9iTjjql+SpHSv4cZkmRv5/zGYW93AlCw==}
engines: {node: '>=18'}
pump@2.0.1:
resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
@ -4073,6 +4116,10 @@ packages:
stubborn-fs@1.2.5:
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:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@ -4118,6 +4165,10 @@ packages:
through2@4.0.2:
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:
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
@ -4527,7 +4578,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.0.15
'@tsclass/tsclass': 9.1.0
'@tsclass/tsclass': 9.2.0
cloudflare: 4.2.0
transitivePeerDependencies:
- encoding
@ -5981,6 +6032,16 @@ snapshots:
public-ip: 6.0.2
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':
dependencies:
'@push.rocks/consolecolor': 2.0.2
@ -6041,6 +6102,11 @@ snapshots:
- typescript
- 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/smartpuppeteer@2.0.5(typescript@5.7.3)':
@ -6848,7 +6914,7 @@ snapshots:
dependencies:
type-fest: 4.40.0
'@tsclass/tsclass@9.1.0':
'@tsclass/tsclass@9.2.0':
dependencies:
type-fest: 4.40.1
@ -6918,6 +6984,8 @@ snapshots:
'@types/default-gateway@3.0.1': {}
'@types/default-gateway@7.2.2': {}
'@types/dns-packet@5.6.5':
dependencies:
'@types/node': 22.15.3
@ -7538,6 +7606,10 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clone-regexp@3.0.0:
dependencies:
is-regexp: 3.1.0
clone@2.1.2: {}
cloudflare@4.2.0:
@ -7614,6 +7686,8 @@ snapshots:
content-type@1.0.5: {}
convert-hrtime@5.0.0: {}
convert-source-map@2.0.0: {}
cookie-signature@1.0.6: {}
@ -8185,6 +8259,8 @@ snapshots:
function-bind@1.1.2: {}
function-timeout@0.1.1: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.2.2:
@ -8295,6 +8371,20 @@ snapshots:
p-cancelable: 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.11: {}
@ -8531,6 +8621,11 @@ snapshots:
dependencies:
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:
dependencies:
call-bind: 1.0.8
@ -8551,6 +8646,8 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
is-regexp@3.1.0: {}
is-stream@2.0.1: {}
is-stream@4.0.1: {}
@ -9590,6 +9687,12 @@ snapshots:
got: 12.6.1
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:
dependencies:
end-of-stream: 1.4.4
@ -10093,6 +10196,12 @@ snapshots:
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:
dependencies:
has-flag: 3.0.0
@ -10158,6 +10267,10 @@ snapshots:
dependencies:
readable-stream: 3.6.2
time-span@5.1.0:
dependencies:
convert-hrtime: 5.0.0
tiny-worker@2.3.0:
dependencies:
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
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
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
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
@ -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:
```typescript
import { SmartAcme } from '@push.rocks/smartacme';
import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
import * as cloudflare from '@apiclient.xyz/cloudflare';
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
// Create a Cloudflare account client with your API 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({
accountEmail: 'youremail@example.com',
mongoDescriptor: {
mongoDbUrl: 'mongodb://yourmongoURL',
mongoDbName: 'yourDbName',
mongoDbPass: 'yourDbPassword',
},
certManager,
environment: 'integration', // 'production' to request real certificates
retryOptions: {}, // optional retry/backoff settings
challengeHandlers: [
retryOptions: {}, // optional retry/backoff settings
challengeHandlers: [ // pluggable ACME challenge handlers
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
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
const mongoDescriptor = {
import { MongoCertManager } from '@push.rocks/smartacme';
const certManager = new MongoCertManager({
mongoDbUrl: 'mongodb://yourmongoURL',
mongoDbName: 'yourDbName',
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
@ -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:
```typescript
import { SmartAcme } from '@push.rocks/smartacme';
import { SmartAcme, MongoCertManager } from '@push.rocks/smartacme';
import * as cloudflare from '@apiclient.xyz/cloudflare';
import { Qenv } from '@push.rocks/qenv';
import { Dns01Handler } from '@push.rocks/smartacme/ts/handlers/Dns01Handler.js';
const qenv = new Qenv('./', './.nogit/');
const cloudflareAccount = new cloudflare.CloudflareAccount(qenv.getEnvVarOnDemand('CF_TOKEN'));
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({
accountEmail: 'youremail@example.com',
mongoDescriptor: {
mongoDbUrl: qenv.getEnvVarRequired('MONGODB_URL'),
mongoDbName: qenv.getEnvVarRequired('MONGODB_DATABASE'),
mongoDbPass: qenv.getEnvVarRequired('MONGODB_PASSWORD'),
},
certManager,
environment: 'integration',
challengeHandlers: [ new Dns01Handler(cloudflareAccount) ],
challengeHandlers: [new Dns01Handler(cloudflareAccount)],
});
await smartAcmeInstance.start();
@ -138,8 +202,8 @@ async function main() {
await smartAcmeInstance.stop();
}
main().catch(console.error);
```
main().catch(console.error);
```
## Built-in Challenge Handlers
@ -222,7 +286,7 @@ async function main() {
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
@ -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.
- **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.
- **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
@ -260,60 +322,13 @@ console.log('Certificate Domain Name:', certDomainName); // Output: example.com
### 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
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();
```bash
pnpm test
```
This comprehensive guide ensures you can set up, manage, and test ACME certificates efficiently and effectively using `@push.rocks/smartacme`.
---

View File

@ -1,16 +1,3 @@
# Plan: Move interestMap from certmanager to smartacme core
## Plan
## Goal
- Pull the interest coordination mechanism out of the ICertManager implementations and into the SmartAcme class.
## Steps
1. Remove `interestMap` from `ICertManager` interface (`ts/interfaces/certmanager.ts`) and its import of `InterestMap`.
2. Strip out `interestMap` property, initialization, and usage from `MemoryCertManager` and `MongoCertManager` (`ts/certmanagers/*.ts`).
3. In `Smartacme` class (`ts/smartacme.classes.smartacme.ts`):
- Add a private `interestMap: plugins.lik.InterestMap<string, SmartacmeCert>` property.
- Initialize it in the constructor: `this.interestMap = new plugins.lik.InterestMap((domain) => domain);`.
- Update `getCertificateForDomain()` and any other consumers to reference `this.interestMap` instead of `this.certmanager.interestMap`.
4. Remove any tests or code that reference the old `interestMap` on `ICertManager` (if any).
5. Run CI (`pnpm build` and `pnpm test`) and fix any regressions.
Please review and confirm before we begin the refactor.
Move the

View File

@ -18,4 +18,12 @@ tap.test('should return undefined for deeper domain', async () => {
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();

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
import * as cloudflare from '@apiclient.xyz/cloudflare';
import { SmartAcme, MongoCertManager } from '../ts/index.js';
import { SmartAcme, certmanagers } from '../ts/index.js';
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
// 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 () => {
smartAcmeInstance = new SmartAcme({
accountEmail: 'domains@lossless.org',
certManager: new MongoCertManager({ mongoDbName, mongoDbPass, mongoDbUrl }),
// certManager: new MongoCertManager({ mongoDbName, mongoDbPass, mongoDbUrl }),
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [new Dns01Handler(cfAccount)],

View File

@ -1,5 +1,6 @@
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';
// Dummy handler for testing
@ -7,12 +8,13 @@ class DummyHandler implements IChallengeHandler<any> {
getSupportedTypes(): string[] { return ['dns-01']; }
async prepare(_: 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 () => {
expect(() => new SmartAcme({
accountEmail: 'test@example.com',
certManager: new MemoryCertManager(),
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
} as any)).toThrow();
@ -21,12 +23,39 @@ tap.test('constructor throws without challengeHandlers', async () => {
tap.test('constructor accepts valid challengeHandlers', async () => {
const sa = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new MemoryCertManager(),
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [new DummyHandler()],
});
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();

View File

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

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 { SmartacmeCert } from '../smartacme.classes.cert.js';
@ -7,12 +7,8 @@ import { SmartacmeCert } from '../smartacme.classes.cert.js';
* 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
@ -24,11 +20,6 @@ export class MemoryCertManager implements ICertManager {
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> {
@ -43,7 +34,5 @@ export class MemoryCertManager implements ICertManager {
*/
public async wipe(): Promise<void> {
this.certs.clear();
// reset interest map
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
}

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 { SmartacmeCert } from '../smartacme.classes.cert.js';
@ -6,7 +6,6 @@ import { SmartacmeCert } from '../smartacme.classes.cert.js';
* 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>>;
@ -20,7 +19,6 @@ export class MongoCertManager implements ICertManager {
'smartacme-certs',
this.db,
);
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
public async init(): Promise<void> {
@ -35,11 +33,6 @@ export class MongoCertManager implements ICertManager {
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> {
@ -55,7 +48,5 @@ export class MongoCertManager implements ICertManager {
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);
}
}

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';
/**

View File

@ -1,5 +1,4 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import * as plugins from '../plugins.js';
import type { IChallengeHandler } from './IChallengeHandler.js';
/**
@ -19,6 +18,9 @@ export class Http01Webroot implements IChallengeHandler<{
keyAuthorization: string;
webPath: string;
}> {
private smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
private smartdnsClient = new plugins.smartdnsClient.Smartdns({});
private webroot: string;
constructor(options: Http01WebrootOptions) {
@ -31,10 +33,10 @@ export class Http01Webroot implements IChallengeHandler<{
public async prepare(ch: { token: string; keyAuthorization: string; webPath: string }): Promise<void> {
const relWebPath = ch.webPath.replace(/^\/+/, '');
const filePath = path.join(this.webroot, relWebPath);
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, ch.keyAuthorization, 'utf8');
const filePath = plugins.path.join(this.webroot, relWebPath);
const dir = plugins.path.dirname(filePath);
await plugins.fs.promises.mkdir(dir, { recursive: true });
await plugins.fs.promises.writeFile(filePath, ch.keyAuthorization, 'utf8');
}
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> {
const relWebPath = ch.webPath.replace(/^\/+/, '');
const filePath = path.join(this.webroot, relWebPath);
const filePath = plugins.path.join(this.webroot, relWebPath);
try {
await fs.unlink(filePath);
await plugins.fs.promises.unlink(filePath);
} catch {
// 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';
/**
@ -13,6 +14,8 @@ export interface 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();
public getSupportedTypes(): string[] {
@ -64,4 +67,17 @@ export class Http01MemoryHandler implements IChallengeHandler<Http01MemoryHandle
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,4 +19,6 @@ export interface IChallengeHandler<T> {
* Clean up resources: remove DNS record, stop server.
*/
cleanup(ch: T): Promise<void>;
checkWetherDomainIsSupported(domainArg: string): Promise<boolean>;
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import * as plugins from './smartacme.plugins.js';
import * as plugins from './plugins.js';
/**
* 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';
/**
@ -10,10 +10,17 @@ export class SmartacmeCertMatcher {
* for wild card certificates
* @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);
// For domains with up to 3 levels (no level4), return base domain.
if (!originalDomain.level4) {
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 { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.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
*/
export interface ISmartAcmeOptions {
accountPrivateKey?: string;
accountEmail: string;
accountPrivateKey?: string;
/**
* Certificate storage manager (e.g., Mongo or in-memory).
*/
@ -64,15 +64,19 @@ export class SmartAcme {
// certificate manager for persistence (implements ICertManager)
public certmanager: ICertManager;
// configured pluggable ACME challenge handlers
public challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
private certmatcher: SmartacmeCertMatcher;
// retry/backoff configuration (resolved with defaults)
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
// track pending DNS challenges for graceful shutdown
private pendingChallenges: plugins.tsclass.network.IDnsChallenge[] = [];
// configured pluggable ACME challenge handlers
private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
// priority order of challenge types
private challengePriority: string[];
// Map for coordinating concurrent certificate requests
private interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
constructor(optionsArg: ISmartAcmeOptions) {
this.options = optionsArg;
@ -98,6 +102,8 @@ export class SmartAcme {
optionsArg.challengePriority && optionsArg.challengePriority.length > 0
? optionsArg.challengePriority
: this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
// initialize interest coordination
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
}
/**
@ -217,14 +223,30 @@ export class SmartAcme {
* @param domainArg
*/
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);
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);
if (
!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;
return certificate;
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
@ -235,7 +257,7 @@ export class SmartAcme {
}
// 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 */
const order = await this.retry(() => this.client.createOrder({