Compare commits

...

21 Commits

Author SHA1 Message Date
3fa34fa373 8.0.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 45m30s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-19 10:01:32 +00:00
086eea1aa2 BREAKING CHANGE(smartacme): Make wildcard certificates opt-in to fix HTTP-01 only configurations 2025-05-19 10:01:31 +00:00
dcc89f0088 7.3.4
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 45m22s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-18 15:47:43 +00:00
695a515990 fix(smartacme): Refine documentation and tests for improved clarity in ACME certificate management 2025-05-18 15:47:42 +00:00
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
23 changed files with 780 additions and 158 deletions

View File

@ -1,5 +1,84 @@
# Changelog
## 2025-05-19 - 8.0.0 - BREAKING CHANGE(smartacme)
Make wildcard certificates opt-in to fix HTTP-01 only configurations
- BREAKING CHANGE: Wildcard certificates are no longer automatically requested for all domains
- Added 'includeWildcard' option to getCertificateForDomain() to explicitly request wildcard certificates
- HTTP-01 only configurations now work correctly as they do not try to request wildcard certificates automatically
- Updated certificate CSR generation to match the requested domain configuration
## 2025-05-19 - 7.4.0 - feat(smartacme)
Make wildcard certificates opt-in to fix HTTP-01 only configurations
- BREAKING CHANGE: Wildcard certificates are no longer automatically requested for all domains
- Added `includeWildcard` option to `getCertificateForDomain()` to explicitly request wildcards
- HTTP-01 only configurations now work correctly as they no longer attempt wildcard certificates
- Wildcard certificates require DNS-01 handler and must be explicitly requested
- Updated certificate CSR generation to match the requested domain configuration
## 2025-05-18 - 7.3.4 - fix(smartacme)
Refine documentation and tests for improved clarity in ACME certificate management
- Enhanced the README with detailed usage, configuration, and example sections
- Refined test cases for certificate matching and challenge handlers across DNS-01 and HTTP-01
- Updated TypeScript definitions and inline comments for better developer experience
## 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

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartacme",
"version": "7.2.1",
"version": "8.0.0",
"private": false,
"description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
"main": "dist_ts/index.js",
@ -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

View File

@ -1,2 +1,13 @@
- this repo is dependent on letsencrypt and its limits
- to simpify the outside API, smartacme is stateful, meaning it works with a mongodb and a collection called 'SmartacmeCert'.
## Certificate Request Behavior
As of v7.4.0, SmartAcme no longer automatically requests wildcard certificates for all domain requests. This change was made to fix issues with HTTP-01 only configurations which cannot validate wildcard domains.
- By default, `getCertificateForDomain('example.com')` only requests a certificate for `example.com`
- To request both regular and wildcard certificates, use `getCertificateForDomain('example.com', { includeWildcard: true })`
- Wildcard certificates require a DNS-01 challenge handler to be configured
- Direct wildcard requests like `getCertificateForDomain('*.example.com')` only request the wildcard certificate
This change ensures HTTP-01 only configurations work properly while still allowing wildcard certificates when needed and supported.

177
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
const smartAcmeInstance = new SmartAcme({
accountEmail: 'youremail@example.com',
mongoDescriptor: {
// 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',
certManager,
environment: 'integration', // 'production' to request real certificates
retryOptions: {}, // optional retry/backoff settings
challengeHandlers: [
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,21 +170,25 @@ 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() {
const smartAcmeInstance = new SmartAcme({
accountEmail: 'youremail@example.com',
mongoDescriptor: {
// 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',
certManager,
environment: 'integration',
challengeHandlers: [new Dns01Handler(cloudflareAccount)],
});
@ -132,9 +196,14 @@ async function main() {
await smartAcmeInstance.start();
const myDomain = 'example.com';
// Get certificate for domain (no wildcard)
const myCert = await smartAcmeInstance.getCertificateForDomain(myDomain);
console.log('Certificate:', myCert);
// Get certificate with wildcard (requires DNS-01 handler)
const certWithWildcard = await smartAcmeInstance.getCertificateForDomain(myDomain, { includeWildcard: true });
console.log('Certificate with wildcard:', certWithWildcard);
await smartAcmeInstance.stop();
}
@ -222,7 +291,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
@ -242,9 +311,10 @@ 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.
- **getCertificateForDomain(domainArg: string, options?: { includeWildcard?: boolean })**: 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.
- By default, only a certificate for the exact domain is requested
- Set `includeWildcard: true` to also request a wildcard certificate (requires DNS-01 handler)
- When requesting a wildcard directly (e.g., `*.example.com`), only the wildcard certificate is requested
### Handling Domain Matching
@ -260,60 +330,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();

178
test/test.http01-only.ts Normal file
View File

@ -0,0 +1,178 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme, certmanagers } from '../ts/index.js';
import { Http01MemoryHandler } from '../ts/handlers/Http01MemoryHandler.js';
// Test that HTTP-01 only configuration works without wildcard certificates
tap.test('HTTP-01 only configuration should work for regular domains', async () => {
const memHandler = new Http01MemoryHandler();
// Stub the domain support check to always return true for testing
memHandler.checkWetherDomainIsSupported = async () => true;
const smartAcmeInstance = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [memHandler],
challengePriority: ['http-01'],
});
// Stub the start method to avoid actual ACME connections
smartAcmeInstance.start = async () => {
smartAcmeInstance.certmatcher = {
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
} as any;
smartAcmeInstance.interestMap = {
checkInterest: async () => false,
addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
} as any;
await smartAcmeInstance.certmanager.init();
};
await smartAcmeInstance.start();
// Stub the core certificate methods to avoid actual ACME calls
smartAcmeInstance.client = {
createOrder: async (orderPayload: any) => {
// Verify no wildcard is included in default request
const identifiers = orderPayload.identifiers;
expect(identifiers.length).toEqual(1);
expect(identifiers[0].value).toEqual('example.com');
expect(identifiers.find((id: any) => id.value.startsWith('*.'))).toBeUndefined();
return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
},
getAuthorizations: async () => [],
finalizeOrder: async () => {},
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
} as any;
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
// Mock certmanager methods
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
// Request certificate without wildcard
const cert = await smartAcmeInstance.getCertificateForDomain('example.com');
expect(cert).toBeDefined();
});
tap.test('should only include wildcard when explicitly requested with DNS-01', async () => {
const dnsHandler = {
getSupportedTypes: () => ['dns-01'],
prepare: async () => {},
cleanup: async () => {},
checkWetherDomainIsSupported: async () => true,
};
const smartAcmeInstance = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [dnsHandler],
challengePriority: ['dns-01'],
});
// Stub the start method to avoid actual ACME connections
smartAcmeInstance.start = async () => {
smartAcmeInstance.certmatcher = {
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
} as any;
smartAcmeInstance.interestMap = {
checkInterest: async () => false,
addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
} as any;
await smartAcmeInstance.certmanager.init();
};
await smartAcmeInstance.start();
// Stub the core certificate methods
smartAcmeInstance.client = {
createOrder: async (orderPayload: any) => {
const identifiers = orderPayload.identifiers;
expect(identifiers.length).toEqual(2);
expect(identifiers[0].value).toEqual('example.com');
expect(identifiers[1].value).toEqual('*.example.com');
return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
},
getAuthorizations: async () => [],
finalizeOrder: async () => {},
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
} as any;
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
// Mock certmanager methods
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
// Request certificate with wildcard
const cert = await smartAcmeInstance.getCertificateForDomain('example.com', { includeWildcard: true });
expect(cert).toBeDefined();
});
tap.test('should skip wildcard if requested but no DNS-01 handler available', async () => {
const httpHandler = new Http01MemoryHandler();
httpHandler.checkWetherDomainIsSupported = async () => true;
const smartAcmeInstance = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [httpHandler],
challengePriority: ['http-01'],
});
// Stub the start method to avoid actual ACME connections
smartAcmeInstance.start = async () => {
smartAcmeInstance.certmatcher = {
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
} as any;
smartAcmeInstance.interestMap = {
checkInterest: async () => false,
addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
} as any;
await smartAcmeInstance.certmanager.init();
};
await smartAcmeInstance.start();
// Mock logger to capture warning
const logSpy = { called: false, message: '' };
smartAcmeInstance.logger.log = async (level: string, message: string) => {
if (level === 'warn') {
logSpy.called = true;
logSpy.message = message;
}
};
// Stub the core certificate methods
smartAcmeInstance.client = {
createOrder: async (orderPayload: any) => {
const identifiers = orderPayload.identifiers;
// Should only have regular domain, no wildcard
expect(identifiers.length).toEqual(1);
expect(identifiers[0].value).toEqual('example.com');
return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
},
getAuthorizations: async () => [],
finalizeOrder: async () => {},
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
} as any;
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
// Mock certmanager methods
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
// Request certificate with wildcard (should be skipped)
const cert = await smartAcmeInstance.getCertificateForDomain('example.com', { includeWildcard: true });
expect(cert).toBeDefined();
expect(logSpy.called).toBeTrue();
expect(logSpy.message).toContain('Wildcard certificate requested but no DNS-01 handler available');
});
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

@ -0,0 +1,94 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme, certmanagers } from '../ts/index.js';
import { SmartacmeCert as Cert } from '../ts/smartacme.classes.cert.js';
// Simple test to verify wildcard options are correctly processed
tap.test('should not include wildcard by default for regular domain', async () => {
let orderPayload: any = null;
// Override the SmartAcme prototype methods for testing
const origGetCert = SmartAcme.prototype.getCertificateForDomain;
// Create a minimal test version of getCertificateForDomain
SmartAcme.prototype.getCertificateForDomain = async function(
domainArg: string,
options?: { includeWildcard?: boolean }
) {
const certDomainName = domainArg.replace('*.', '');
const identifiers = [];
if (domainArg.startsWith('*.')) {
identifiers.push({ type: 'dns', value: domainArg });
} else {
identifiers.push({ type: 'dns', value: certDomainName });
if (options?.includeWildcard) {
const hasDnsHandler = this.challengeHandlers.some((h) =>
h.getSupportedTypes().includes('dns-01')
);
if (hasDnsHandler) {
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
}
}
}
orderPayload = { identifiers };
return new Cert({ domainName: certDomainName });
};
try {
// Create instance with HTTP-01 only
const smartAcme = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
challengeHandlers: [{
getSupportedTypes: () => ['http-01'],
prepare: async () => {},
cleanup: async () => {},
checkWetherDomainIsSupported: async () => true,
}],
});
// Test 1: Regular domain without wildcard option
await smartAcme.getCertificateForDomain('example.com');
expect(orderPayload.identifiers.length).toEqual(1);
expect(orderPayload.identifiers[0].value).toEqual('example.com');
// Test 2: Regular domain with wildcard option (but no DNS-01 handler)
await smartAcme.getCertificateForDomain('example.com', { includeWildcard: true });
expect(orderPayload.identifiers.length).toEqual(1);
expect(orderPayload.identifiers[0].value).toEqual('example.com');
// Create instance with DNS-01
const smartAcmeDns = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
challengeHandlers: [{
getSupportedTypes: () => ['dns-01'],
prepare: async () => {},
cleanup: async () => {},
checkWetherDomainIsSupported: async () => true,
}],
});
// Test 3: Regular domain with wildcard option and DNS-01 handler
await smartAcmeDns.getCertificateForDomain('example.com', { includeWildcard: true });
expect(orderPayload.identifiers.length).toEqual(2);
expect(orderPayload.identifiers[0].value).toEqual('example.com');
expect(orderPayload.identifiers[1].value).toEqual('*.example.com');
// Test 4: Direct wildcard request
await smartAcmeDns.getCertificateForDomain('*.example.com');
expect(orderPayload.identifiers.length).toEqual(1);
expect(orderPayload.identifiers[0].value).toEqual('*.example.com');
} finally {
// Restore original method
SmartAcme.prototype.getCertificateForDomain = origGetCert;
}
});
export default tap.start();

View File

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

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

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,13 +64,15 @@ 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
@ -219,28 +221,30 @@ export class SmartAcme {
* * retrieve it from the databse and return it
*
* @param domainArg
* @param options Optional configuration for certificate generation
*/
public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> {
public async getCertificateForDomain(
domainArg: string,
options?: { includeWildcard?: boolean }
): 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);
// integration test stub: bypass ACME and return a dummy certificate
if (this.options.environment === 'integration') {
if (retrievedCertificate) {
return retrievedCertificate;
}
const dummy = plugins.smartunique.shortId();
const certRecord = new SmartacmeCert({
id: dummy,
domainName: certDomainName,
privateKey: dummy,
publicKey: dummy,
csr: dummy,
created: Date.now(),
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
});
await this.certmanager.storeCertificate(certRecord);
return certRecord;
}
if (
!retrievedCertificate &&
@ -259,12 +263,32 @@ export class SmartAcme {
// lets make sure others get the same interest
const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
// Build identifiers array based on request
const identifiers = [];
if (isWildcardRequest) {
// If requesting a wildcard directly, only add the wildcard
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
} else {
// Add the regular domain
identifiers.push({ type: 'dns', value: certDomainName });
// Only add wildcard if explicitly requested
if (options?.includeWildcard) {
const hasDnsHandler = this.challengeHandlers.some((h) =>
h.getSupportedTypes().includes('dns-01'),
);
if (!hasDnsHandler) {
this.logger.log('warn', 'Wildcard certificate requested but no DNS-01 handler available. Skipping wildcard.');
} else {
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
}
}
}
/* Place new order with retry */
const order = await this.retry(() => this.client.createOrder({
identifiers: [
{ type: 'dns', value: certDomainName },
{ type: 'dns', value: `*.${certDomainName}` },
],
identifiers,
}), 'createOrder');
/* Get authorizations and select challenges */
@ -359,9 +383,25 @@ export class SmartAcme {
}
/* Finalize order */
const csrDomains = [];
let commonName: string;
if (isWildcardRequest) {
// For wildcard requests, use wildcard as common name
commonName = `*.${certDomainName}`;
csrDomains.push(certDomainName); // Add base domain as alt name
} else {
// For regular requests, use base domain as common name
commonName = certDomainName;
if (options?.includeWildcard && identifiers.some(id => id.value === `*.${certDomainName}`)) {
// If wildcard was successfully added, include it as alt name
csrDomains.push(`*.${certDomainName}`);
}
}
const [key, csr] = await plugins.acme.forge.createCsr({
commonName: `*.${certDomainName}`,
altNames: [certDomainName],
commonName,
altNames: csrDomains,
});
await this.retry(() => this.client.finalizeOrder(order, csr), 'finalizeOrder');