Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
b8bb4af184 | |||
6fedf0505e | |||
f814038a6a | |||
9dc8c1d8a3 | |||
758c6c6b5d | |||
6363ec4be6 | |||
6a53346d14 | |||
fc420eb615 | |||
9f66a0487f | |||
40cae220d0 | |||
f7dccb25e4 | |||
da75c52c09 | |||
708145c550 | |||
0de2178eb5 |
53
changelog.md
53
changelog.md
@ -1,5 +1,58 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-01 - 7.2.0 - feat(core)
|
||||||
|
Refactor SmartAcme core to centralize interest coordination and update dependencies
|
||||||
|
|
||||||
|
- Moved interest coordination mechanism out of ICertManager implementations and into SmartAcme core
|
||||||
|
- Updated certificate managers (MemoryCertManager and MongoCertManager) to remove redundant interestMap handling
|
||||||
|
- Upgraded @push.rocks/tapbundle from 6.0.1 to 6.0.3 in package.json
|
||||||
|
- Revised readme.plan.md to reflect the new interest coordination approach
|
||||||
|
|
||||||
|
## 2025-04-30 - 7.1.0 - feat(certmanagers/integration)
|
||||||
|
Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency
|
||||||
|
|
||||||
|
- Introduce wipe() in ICertManager to support integration testing by clearing stored certificates
|
||||||
|
- Implement wipe() in MemoryCertManager and MongoCertManager for resetting internal state
|
||||||
|
- Refactor SmartAcme constructor to consider wiping certificates in integration mode (commented out for now)
|
||||||
|
- Update integration test assertions and add console logging for domain certificate retrieval
|
||||||
|
- Upgrade @push.rocks/tapbundle from ^6.0.0 to ^6.0.1
|
||||||
|
|
||||||
|
## 2025-04-30 - 7.0.0 - BREAKING CHANGE(SmartAcme (Cert Management))
|
||||||
|
Refactor certificate management and challenge handling API to use a unified certManager interface, remove legacy storage, and update challenge workflows.
|
||||||
|
|
||||||
|
- Introduce ICertManager interface with MemoryCertManager and MongoCertManager implementations.
|
||||||
|
- Remove the legacy SmartacmeCertManager and update SmartAcme to require a certManager option instead of mongoDescriptor.
|
||||||
|
- Adjust certificate renewal logic to delete and store certificates through the new certManager API.
|
||||||
|
- Refine DNS-01 challenge handling by removing in-handler DNS propagation waiting and relying on external checks.
|
||||||
|
- Increase retry settings for robustness during challenge verification and certificate issuance.
|
||||||
|
- Update integration and unit tests to use the new certManager configuration.
|
||||||
|
|
||||||
|
## 2025-04-30 - 6.2.0 - feat(handlers)
|
||||||
|
Add in-memory HTTP-01 challenge handler and rename file-based handler to Http01Webroot
|
||||||
|
|
||||||
|
- Renamed Http01Handler to Http01Webroot in both implementation and documentation
|
||||||
|
- Introduced Http01MemoryHandler for diskless HTTP-01 challenges
|
||||||
|
- Updated tests and README examples to reflect handler name changes and new feature
|
||||||
|
|
||||||
|
## 2025-04-30 - 6.1.3 - fix(Dns01Handler)
|
||||||
|
Update dependency versions and refine Dns01Handler implementation
|
||||||
|
|
||||||
|
- Bump '@apiclient.xyz/cloudflare' to ^6.4.1 and '@tsclass/tsclass' to ^9.1.0 in package.json
|
||||||
|
- Remove duplicate Cloudflare import in smartacme.plugins.ts
|
||||||
|
- Refactor Dns01Handler to use IConvenientDnsProvider and add checkWetherDomainIsSupported method
|
||||||
|
- Align devDependencies versions for improved consistency
|
||||||
|
|
||||||
|
## 2025-04-27 - 6.1.2 - fix(repo)
|
||||||
|
Update repository metadata by replacing the LICENSE file with a license.md file for improved consistency.
|
||||||
|
|
||||||
|
- Removed the old LICENSE file.
|
||||||
|
- Introduced license.md as the new license documentation file.
|
||||||
|
|
||||||
|
## 2025-04-27 - 6.1.1 - fix(readme)
|
||||||
|
Fix license link reference in documentation
|
||||||
|
|
||||||
|
- Updated the license link from [license](license) to [license.md](license.md) in the License and Legal Information section
|
||||||
|
|
||||||
## 2025-04-27 - 6.1.0 - feat(readme)
|
## 2025-04-27 - 6.1.0 - feat(readme)
|
||||||
Update documentation with detailed built-in challenge handlers and custom handler examples
|
Update documentation with detailed built-in challenge handlers and custom handler examples
|
||||||
|
|
||||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartacme",
|
"name": "@push.rocks/smartacme",
|
||||||
"version": "6.1.0",
|
"version": "7.2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
|
"description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@ -40,6 +40,7 @@
|
|||||||
"homepage": "https://code.foss.global/push.rocks/smartacme#readme",
|
"homepage": "https://code.foss.global/push.rocks/smartacme#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedserver": "^3.0.74",
|
"@api.global/typedserver": "^3.0.74",
|
||||||
|
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
"@push.rocks/smartdata": "^5.15.1",
|
"@push.rocks/smartdata": "^5.15.1",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
@ -50,17 +51,16 @@
|
|||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"@push.rocks/smarttime": "^4.1.1",
|
"@push.rocks/smarttime": "^4.1.1",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@tsclass/tsclass": "^9.0.0",
|
"@tsclass/tsclass": "^9.1.0",
|
||||||
"acme-client": "^5.4.0"
|
"acme-client": "^5.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apiclient.xyz/cloudflare": "^6.3.2",
|
|
||||||
"@git.zone/tsbuild": "^2.3.2",
|
"@git.zone/tsbuild": "^2.3.2",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^1.0.96",
|
"@git.zone/tstest": "^1.0.96",
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
"@push.rocks/qenv": "^6.1.0",
|
||||||
"@push.rocks/tapbundle": "^5.6.3",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
"@types/node": "^22.15.2"
|
"@types/node": "^22.15.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
154
pnpm-lock.yaml
generated
154
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
|||||||
'@api.global/typedserver':
|
'@api.global/typedserver':
|
||||||
specifier: ^3.0.74
|
specifier: ^3.0.74
|
||||||
version: 3.0.74
|
version: 3.0.74
|
||||||
|
'@apiclient.xyz/cloudflare':
|
||||||
|
specifier: ^6.4.1
|
||||||
|
version: 6.4.1
|
||||||
'@push.rocks/lik':
|
'@push.rocks/lik':
|
||||||
specifier: ^6.2.2
|
specifier: ^6.2.2
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
@ -42,15 +45,12 @@ importers:
|
|||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.0.0
|
specifier: ^9.1.0
|
||||||
version: 9.0.0
|
version: 9.1.0
|
||||||
acme-client:
|
acme-client:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.4.0
|
version: 5.4.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@apiclient.xyz/cloudflare':
|
|
||||||
specifier: ^6.3.2
|
|
||||||
version: 6.3.2
|
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
@ -64,11 +64,11 @@ importers:
|
|||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
'@push.rocks/tapbundle':
|
'@push.rocks/tapbundle':
|
||||||
specifier: ^5.6.3
|
specifier: ^6.0.3
|
||||||
version: 5.6.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
|
version: 6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.15.2
|
specifier: ^22.15.3
|
||||||
version: 22.15.2
|
version: 22.15.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -87,8 +87,8 @@ packages:
|
|||||||
'@api.global/typedsocket@3.0.1':
|
'@api.global/typedsocket@3.0.1':
|
||||||
resolution: {integrity: sha512-xojiAVNXtHoxkpBo8U2HHJG8FrVXXuLvDNndSHXwx4C9VslUwDn5zSCI+PdBl8iAg+ZuBmKjqkpZZ9sL6DC5yQ==}
|
resolution: {integrity: sha512-xojiAVNXtHoxkpBo8U2HHJG8FrVXXuLvDNndSHXwx4C9VslUwDn5zSCI+PdBl8iAg+ZuBmKjqkpZZ9sL6DC5yQ==}
|
||||||
|
|
||||||
'@apiclient.xyz/cloudflare@6.3.2':
|
'@apiclient.xyz/cloudflare@6.4.1':
|
||||||
resolution: {integrity: sha512-u5ud25tR1epNVgAPtL2t1qZ7FOGsLhID4zAzwcIQQTqmBb43US0fkI/I+JjIW0uyHi12AI4gWez2ke2nAR4+pw==}
|
resolution: {integrity: sha512-RYFphnbunjK+Imq/3ynIQpAvIGBJ38kqSZ2nrpTm26zsBIxW7S6xEe3zhXfVMtUIgC99OL3Xr/SGXl3CNBwCug==}
|
||||||
|
|
||||||
'@aws-crypto/crc32@5.2.0':
|
'@aws-crypto/crc32@5.2.0':
|
||||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||||
@ -821,6 +821,9 @@ packages:
|
|||||||
'@push.rocks/smartexpect@1.6.1':
|
'@push.rocks/smartexpect@1.6.1':
|
||||||
resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==}
|
resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==}
|
||||||
|
|
||||||
|
'@push.rocks/smartexpect@2.4.2':
|
||||||
|
resolution: {integrity: sha512-L+aS1n5rWhf/yOh5R3zPgwycYtDr5FfrDWgasy6ShhN6Zbn/z/AOPbWcF/OpeTmx0XabWB2h5d4xBcCKLl47cQ==}
|
||||||
|
|
||||||
'@push.rocks/smartfeed@1.0.11':
|
'@push.rocks/smartfeed@1.0.11':
|
||||||
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
|
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
|
||||||
|
|
||||||
@ -956,6 +959,9 @@ packages:
|
|||||||
'@push.rocks/tapbundle@5.6.3':
|
'@push.rocks/tapbundle@5.6.3':
|
||||||
resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==}
|
resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==}
|
||||||
|
|
||||||
|
'@push.rocks/tapbundle@6.0.3':
|
||||||
|
resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
'@push.rocks/taskbuffer@3.1.7':
|
||||||
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
||||||
|
|
||||||
@ -1336,8 +1342,8 @@ packages:
|
|||||||
'@tsclass/tsclass@8.2.1':
|
'@tsclass/tsclass@8.2.1':
|
||||||
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
|
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
|
||||||
|
|
||||||
'@tsclass/tsclass@9.0.0':
|
'@tsclass/tsclass@9.1.0':
|
||||||
resolution: {integrity: sha512-QuV2WKzi3p1ONq0UR+hNulG62D6vRPJxOXunWvN9zpWx6Uj70DKntMu8nqEIWUPgL3UKIPe7GN8l6mPCdxdcEg==}
|
resolution: {integrity: sha512-PkG1bXK/bqVtxaRHje+iJHjtcdRHLHrNTOkzqh+jv2A7mgiyNo2YBJIl4eEJLkw1X3FwEFU4vCAtsegSmJgRug==}
|
||||||
|
|
||||||
'@types/accepts@1.3.7':
|
'@types/accepts@1.3.7':
|
||||||
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
|
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
|
||||||
@ -1499,8 +1505,8 @@ packages:
|
|||||||
'@types/node@18.19.87':
|
'@types/node@18.19.87':
|
||||||
resolution: {integrity: sha512-OIAAu6ypnVZHmsHCeJ+7CCSub38QNBS9uceMQeg7K5Ur0Jr+wG9wEOEvvMbhp09pxD5czIUy/jND7s7Tb6Nw7A==}
|
resolution: {integrity: sha512-OIAAu6ypnVZHmsHCeJ+7CCSub38QNBS9uceMQeg7K5Ur0Jr+wG9wEOEvvMbhp09pxD5czIUy/jND7s7Tb6Nw7A==}
|
||||||
|
|
||||||
'@types/node@22.15.2':
|
'@types/node@22.15.3':
|
||||||
resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==}
|
resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==}
|
||||||
|
|
||||||
'@types/parse5@6.0.3':
|
'@types/parse5@6.0.3':
|
||||||
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
||||||
@ -4189,6 +4195,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==}
|
resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
type-fest@4.40.1:
|
||||||
|
resolution: {integrity: sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
type-is@1.6.18:
|
type-is@1.6.18:
|
||||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -4510,14 +4520,14 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@apiclient.xyz/cloudflare@6.3.2':
|
'@apiclient.xyz/cloudflare@6.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.0.7
|
'@push.rocks/smartlog': 3.0.7
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 2.1.0
|
'@push.rocks/smartrequest': 2.1.0
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.0.15
|
||||||
'@tsclass/tsclass': 5.0.0
|
'@tsclass/tsclass': 9.1.0
|
||||||
cloudflare: 4.2.0
|
cloudflare: 4.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
@ -5399,7 +5409,7 @@ snapshots:
|
|||||||
'@jest/schemas': 29.6.3
|
'@jest/schemas': 29.6.3
|
||||||
'@types/istanbul-lib-coverage': 2.0.6
|
'@types/istanbul-lib-coverage': 2.0.6
|
||||||
'@types/istanbul-reports': 3.0.4
|
'@types/istanbul-reports': 3.0.4
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
'@types/yargs': 17.0.33
|
'@types/yargs': 17.0.33
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
||||||
@ -5820,6 +5830,12 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
|
|
||||||
|
'@push.rocks/smartexpect@2.4.2':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
|
||||||
'@push.rocks/smartfeed@1.0.11':
|
'@push.rocks/smartfeed@1.0.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tsclass/tsclass': 3.0.48
|
'@tsclass/tsclass': 3.0.48
|
||||||
@ -6224,6 +6240,38 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@push.rocks/tapbundle@6.0.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)':
|
||||||
|
dependencies:
|
||||||
|
'@open-wc/testing': 4.0.0
|
||||||
|
'@push.rocks/consolecolor': 2.0.2
|
||||||
|
'@push.rocks/qenv': 6.1.0
|
||||||
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartenv': 5.0.12
|
||||||
|
'@push.rocks/smartexpect': 2.4.2
|
||||||
|
'@push.rocks/smartfile': 11.2.0
|
||||||
|
'@push.rocks/smartjson': 5.0.20
|
||||||
|
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
|
||||||
|
'@push.rocks/smartpath': 5.0.18
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrequest': 2.1.0
|
||||||
|
'@push.rocks/smarts3': 2.2.5
|
||||||
|
'@push.rocks/smartshell': 3.2.3
|
||||||
|
'@push.rocks/smarttime': 4.1.1
|
||||||
|
expect: 29.7.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@aws-sdk/credential-providers'
|
||||||
|
- '@mongodb-js/zstd'
|
||||||
|
- aws-crt
|
||||||
|
- bufferutil
|
||||||
|
- gcp-metadata
|
||||||
|
- kerberos
|
||||||
|
- mongodb-client-encryption
|
||||||
|
- snappy
|
||||||
|
- socks
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
'@push.rocks/taskbuffer@3.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@ -6800,24 +6848,24 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 4.40.0
|
type-fest: 4.40.0
|
||||||
|
|
||||||
'@tsclass/tsclass@9.0.0':
|
'@tsclass/tsclass@9.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 4.40.0
|
type-fest: 4.40.1
|
||||||
|
|
||||||
'@types/accepts@1.3.7':
|
'@types/accepts@1.3.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/babel__code-frame@7.0.6': {}
|
'@types/babel__code-frame@7.0.6': {}
|
||||||
|
|
||||||
'@types/bn.js@5.1.6':
|
'@types/bn.js@5.1.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/body-parser@1.19.5':
|
'@types/body-parser@1.19.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/buffer-json@2.0.3': {}
|
'@types/buffer-json@2.0.3': {}
|
||||||
|
|
||||||
@ -6833,17 +6881,17 @@ snapshots:
|
|||||||
|
|
||||||
'@types/clean-css@4.2.11':
|
'@types/clean-css@4.2.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
'@types/co-body@6.1.3':
|
'@types/co-body@6.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
'@types/qs': 6.9.18
|
'@types/qs': 6.9.18
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/content-disposition@0.5.8': {}
|
'@types/content-disposition@0.5.8': {}
|
||||||
|
|
||||||
@ -6854,11 +6902,11 @@ snapshots:
|
|||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/express': 5.0.1
|
'@types/express': 5.0.1
|
||||||
'@types/keygrip': 1.0.6
|
'@types/keygrip': 1.0.6
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/cors@2.8.17':
|
'@types/cors@2.8.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/debounce@1.2.4': {}
|
'@types/debounce@1.2.4': {}
|
||||||
|
|
||||||
@ -6872,7 +6920,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/dns-packet@5.6.5':
|
'@types/dns-packet@5.6.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/elliptic@6.4.18':
|
'@types/elliptic@6.4.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6880,7 +6928,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@5.0.6':
|
'@types/express-serve-static-core@5.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
'@types/qs': 6.9.18
|
'@types/qs': 6.9.18
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
@ -6897,30 +6945,30 @@ snapshots:
|
|||||||
|
|
||||||
'@types/from2@2.3.5':
|
'@types/from2@2.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/fs-extra@11.0.4':
|
'@types/fs-extra@11.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/jsonfile': 6.1.4
|
'@types/jsonfile': 6.1.4
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/fs-extra@9.0.13':
|
'@types/fs-extra@9.0.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/glob@7.2.0':
|
'@types/glob@7.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/glob@8.1.0':
|
'@types/glob@8.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/gunzip-maybe@1.4.2':
|
'@types/gunzip-maybe@1.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6954,7 +7002,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/keygrip@1.0.6': {}
|
'@types/keygrip@1.0.6': {}
|
||||||
|
|
||||||
@ -6971,7 +7019,7 @@ snapshots:
|
|||||||
'@types/http-errors': 2.0.4
|
'@types/http-errors': 2.0.4
|
||||||
'@types/keygrip': 1.0.6
|
'@types/keygrip': 1.0.6
|
||||||
'@types/koa-compose': 3.2.8
|
'@types/koa-compose': 3.2.8
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6989,18 +7037,18 @@ snapshots:
|
|||||||
|
|
||||||
'@types/node-fetch@2.6.12':
|
'@types/node-fetch@2.6.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
form-data: 4.0.2
|
form-data: 4.0.2
|
||||||
|
|
||||||
'@types/node-forge@1.3.11':
|
'@types/node-forge@1.3.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/node@18.19.87':
|
'@types/node@18.19.87':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
|
|
||||||
'@types/node@22.15.2':
|
'@types/node@22.15.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
@ -7018,19 +7066,19 @@ snapshots:
|
|||||||
|
|
||||||
'@types/s3rver@3.7.4':
|
'@types/s3rver@3.7.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/semver@7.7.0': {}
|
'@types/semver@7.7.0': {}
|
||||||
|
|
||||||
'@types/send@0.17.4':
|
'@types/send@0.17.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime': 1.3.5
|
'@types/mime': 1.3.5
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/serve-static@1.15.7':
|
'@types/serve-static@1.15.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.4
|
'@types/http-errors': 2.0.4
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
|
|
||||||
'@types/sinon-chai@3.2.12':
|
'@types/sinon-chai@3.2.12':
|
||||||
@ -7050,11 +7098,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/tar-stream@2.2.3':
|
'@types/tar-stream@2.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/through2@2.0.41':
|
'@types/through2@2.0.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/triple-beam@1.3.5': {}
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
@ -7078,18 +7126,18 @@ snapshots:
|
|||||||
|
|
||||||
'@types/whatwg-url@8.2.2':
|
'@types/whatwg-url@8.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
'@types/webidl-conversions': 7.0.3
|
'@types/webidl-conversions': 7.0.3
|
||||||
|
|
||||||
'@types/which@3.0.4': {}
|
'@types/which@3.0.4': {}
|
||||||
|
|
||||||
'@types/ws@7.4.7':
|
'@types/ws@7.4.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
|
|
||||||
@ -7099,7 +7147,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
@ -7775,7 +7823,7 @@ snapshots:
|
|||||||
engine.io@6.6.4:
|
engine.io@6.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cors': 2.8.17
|
'@types/cors': 2.8.17
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
@ -8579,7 +8627,7 @@ snapshots:
|
|||||||
jest-util@29.7.0:
|
jest-util@29.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
'@types/node': 22.15.2
|
'@types/node': 22.15.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 3.9.0
|
ci-info: 3.9.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@ -10172,6 +10220,8 @@ snapshots:
|
|||||||
|
|
||||||
type-fest@4.40.0: {}
|
type-fest@4.40.0: {}
|
||||||
|
|
||||||
|
type-fest@4.40.1: {}
|
||||||
|
|
||||||
type-is@1.6.18:
|
type-is@1.6.18:
|
||||||
dependencies:
|
dependencies:
|
||||||
media-typer: 0.3.0
|
media-typer: 0.3.0
|
||||||
|
27
readme.md
27
readme.md
@ -61,7 +61,7 @@ const smartAcmeInstance = new SmartAcme({
|
|||||||
retryOptions: {}, // optional retry/backoff settings
|
retryOptions: {}, // optional retry/backoff settings
|
||||||
challengeHandlers: [
|
challengeHandlers: [
|
||||||
new Dns01Handler(cfAccount),
|
new Dns01Handler(cfAccount),
|
||||||
// you can add more handlers, e.g. Http01Handler
|
// you can add more handlers, e.g. Http01Webroot
|
||||||
],
|
],
|
||||||
challengePriority: ['dns-01'], // optional ordering of challenge types
|
challengePriority: ['dns-01'], // optional ordering of challenge types
|
||||||
});
|
});
|
||||||
@ -143,7 +143,7 @@ async function main() {
|
|||||||
|
|
||||||
## Built-in Challenge Handlers
|
## Built-in Challenge Handlers
|
||||||
|
|
||||||
This module includes two out-of-the-box ACME challenge handlers:
|
This module includes three out-of-the-box ACME challenge handlers:
|
||||||
|
|
||||||
- **Dns01Handler**
|
- **Dns01Handler**
|
||||||
- Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation.
|
- Uses a Cloudflare account (from `@apiclient.xyz/cloudflare`) and Smartdns client to set and remove DNS TXT records, then wait for propagation.
|
||||||
@ -158,18 +158,31 @@ async function main() {
|
|||||||
const dnsHandler = new Dns01Handler(cfAccount);
|
const dnsHandler = new Dns01Handler(cfAccount);
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Http01Handler**
|
- **Http01Webroot**
|
||||||
- Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup.
|
- Writes ACME HTTP-01 challenge files under a file-system webroot (`/.well-known/acme-challenge/`), and removes them on cleanup.
|
||||||
- Import path:
|
- Import path:
|
||||||
```typescript
|
```typescript
|
||||||
import { Http01Handler } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js';
|
import { Http01Webroot } from '@push.rocks/smartacme/ts/handlers/Http01Handler.js';
|
||||||
```
|
```
|
||||||
- Example:
|
- Example:
|
||||||
```typescript
|
```typescript
|
||||||
const httpHandler = new Http01Handler({ webroot: '/var/www/html' });
|
const httpHandler = new Http01Webroot({ webroot: '/var/www/html' });
|
||||||
```
|
```
|
||||||
|
|
||||||
Both handlers implement the `IChallengeHandler<T>` interface and can be combined in the `challengeHandlers` array.
|
- **Http01MemoryHandler**
|
||||||
|
- In-memory HTTP-01 challenge handler that stores and serves ACME tokens without disk I/O.
|
||||||
|
- Import path:
|
||||||
|
```typescript
|
||||||
|
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
|
||||||
|
```
|
||||||
|
- Example (Express integration):
|
||||||
|
```typescript
|
||||||
|
import { Http01MemoryHandler } from '@push.rocks/smartacme/ts/handlers/Http01MemoryHandler.js';
|
||||||
|
const memoryHandler = new Http01MemoryHandler();
|
||||||
|
app.use((req, res, next) => memoryHandler.handleRequest(req, res, next));
|
||||||
|
```
|
||||||
|
|
||||||
|
All handlers implement the `IChallengeHandler<T>` interface and can be combined in the `challengeHandlers` array.
|
||||||
|
|
||||||
## Creating Custom Handlers
|
## Creating Custom Handlers
|
||||||
|
|
||||||
@ -307,7 +320,7 @@ This comprehensive guide ensures you can set up, manage, and test ACME certifica
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license.md](license.md) file within this repository.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
16
readme.plan.md
Normal file
16
readme.plan.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Plan: Move interestMap from certmanager to smartacme core
|
||||||
|
|
||||||
|
## 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.
|
@ -7,15 +7,11 @@ tap.test('Dns01Handler prepare and cleanup calls Cloudflare and DNS functions',
|
|||||||
// fake Cloudflare API
|
// fake Cloudflare API
|
||||||
const fakeCF: any = {
|
const fakeCF: any = {
|
||||||
convenience: {
|
convenience: {
|
||||||
acmeSetDnsChallenge: async (ch: any) => {
|
acmeSetDnsChallenge: async (_ch: any) => {
|
||||||
setCalled = true;
|
setCalled = true;
|
||||||
expect(ch).toHaveProperty('hostName');
|
|
||||||
expect(ch).toHaveProperty('challenge');
|
|
||||||
},
|
},
|
||||||
acmeRemoveDnsChallenge: async (ch: any) => {
|
acmeRemoveDnsChallenge: async (_ch: any) => {
|
||||||
removeCalled = true;
|
removeCalled = true;
|
||||||
expect(ch).toHaveProperty('hostName');
|
|
||||||
expect(ch).toHaveProperty('challenge');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
58
test/test.handlers-http01-memory.ts
Normal file
58
test/test.handlers-http01-memory.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
|
import { Http01MemoryHandler } from '../ts/handlers/Http01MemoryHandler.js';
|
||||||
|
|
||||||
|
tap.test('Http01MemoryHandler serves in-memory challenges and cleans up', async () => {
|
||||||
|
const handler = new Http01MemoryHandler();
|
||||||
|
const token = 'testtoken';
|
||||||
|
const keyAuth = 'keyAuthValue';
|
||||||
|
const webPath = `/.well-known/acme-challenge/${token}`;
|
||||||
|
const challenge = { type: 'http-01', token, keyAuthorization: keyAuth, webPath };
|
||||||
|
|
||||||
|
// Prepare challenge (store in memory)
|
||||||
|
await handler.prepare(challenge);
|
||||||
|
|
||||||
|
// Serve existing challenge without next()
|
||||||
|
const req1: any = { url: webPath };
|
||||||
|
const res1: any = {
|
||||||
|
statusCode: 0,
|
||||||
|
headers: {} as Record<string, string>,
|
||||||
|
body: '',
|
||||||
|
setHeader(name: string, value: string) { this.headers[name] = value; },
|
||||||
|
end(body?: string) { this.body = body || ''; },
|
||||||
|
};
|
||||||
|
handler.handleRequest(req1, res1);
|
||||||
|
expect(res1.statusCode).toEqual(200);
|
||||||
|
expect(res1.body).toEqual(keyAuth);
|
||||||
|
expect(res1.headers['content-type']).toEqual('text/plain');
|
||||||
|
|
||||||
|
// Cleanup challenge (remove from memory)
|
||||||
|
await handler.cleanup(challenge);
|
||||||
|
|
||||||
|
// Serve after cleanup without next() should give 404
|
||||||
|
const req2: any = { url: webPath };
|
||||||
|
const res2: any = {
|
||||||
|
statusCode: 0,
|
||||||
|
headers: {} as Record<string, string>,
|
||||||
|
body: '',
|
||||||
|
setHeader(name: string, value: string) { this.headers[name] = value; },
|
||||||
|
end(body?: string) { this.body = body || ''; },
|
||||||
|
};
|
||||||
|
handler.handleRequest(req2, res2);
|
||||||
|
expect(res2.statusCode).toEqual(404);
|
||||||
|
|
||||||
|
// Serve after cleanup with next() should call next
|
||||||
|
const req3: any = { url: webPath };
|
||||||
|
let nextCalled = false;
|
||||||
|
const next = () => { nextCalled = true; };
|
||||||
|
const res3: any = {
|
||||||
|
statusCode: 0,
|
||||||
|
headers: {} as Record<string, string>,
|
||||||
|
body: '',
|
||||||
|
setHeader(name: string, value: string) { this.headers[name] = value; },
|
||||||
|
end(body?: string) { this.body = body || ''; },
|
||||||
|
};
|
||||||
|
handler.handleRequest(req3, res3, next);
|
||||||
|
expect(nextCalled).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@ -1,13 +1,13 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
import { Http01Handler } from '../ts/handlers/Http01Handler.js';
|
import { Http01Webroot } from '../ts/handlers/Http01Handler.js';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
tap.test('Http01Handler writes challenge file and removes it on cleanup', async () => {
|
tap.test('Http01Webroot writes challenge file and removes it on cleanup', async () => {
|
||||||
// create temporary webroot directory
|
// create temporary webroot directory
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'http01-'));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'http01-'));
|
||||||
const handler = new Http01Handler({ webroot: tmpDir });
|
const handler = new Http01Webroot({ webroot: tmpDir });
|
||||||
const token = 'testtoken';
|
const token = 'testtoken';
|
||||||
const keyAuth = 'keyAuthValue';
|
const keyAuth = 'keyAuthValue';
|
||||||
const webPath = `/.well-known/acme-challenge/${token}`;
|
const webPath = `/.well-known/acme-challenge/${token}`;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
import { Qenv } from '@push.rocks/qenv';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||||
import { SmartAcme } from '../ts/index.js';
|
import { SmartAcme, MongoCertManager } from '../ts/index.js';
|
||||||
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
|
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
|
||||||
|
|
||||||
// Load environment variables for credentials (stored under .nogit/)
|
// Load environment variables for credentials (stored under .nogit/)
|
||||||
@ -14,12 +14,13 @@ const mongoDbName = (await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'))!;
|
|||||||
const mongoDbPass = (await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'))!;
|
const mongoDbPass = (await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'))!;
|
||||||
const mongoDbUrl = (await testQenv.getEnvVarOnDemand('MONGODB_URL'))!;
|
const mongoDbUrl = (await testQenv.getEnvVarOnDemand('MONGODB_URL'))!;
|
||||||
|
|
||||||
|
|
||||||
let smartAcmeInstance: SmartAcme;
|
let smartAcmeInstance: SmartAcme;
|
||||||
|
|
||||||
tap.test('create SmartAcme instance with DNS-01 handler and start', async () => {
|
tap.test('create SmartAcme instance with DNS-01 handler and start', async () => {
|
||||||
smartAcmeInstance = new SmartAcme({
|
smartAcmeInstance = new SmartAcme({
|
||||||
accountEmail: 'domains@lossless.org',
|
accountEmail: 'domains@lossless.org',
|
||||||
mongoDescriptor: { mongoDbName, mongoDbPass, mongoDbUrl },
|
certManager: new MongoCertManager({ mongoDbName, mongoDbPass, mongoDbUrl }),
|
||||||
environment: 'integration',
|
environment: 'integration',
|
||||||
retryOptions: {},
|
retryOptions: {},
|
||||||
challengeHandlers: [new Dns01Handler(cfAccount)],
|
challengeHandlers: [new Dns01Handler(cfAccount)],
|
||||||
@ -29,6 +30,10 @@ tap.test('create SmartAcme instance with DNS-01 handler and start', async () =>
|
|||||||
expect(smartAcmeInstance).toBeInstanceOf(SmartAcme);
|
expect(smartAcmeInstance).toBeInstanceOf(SmartAcme);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('should wipe the certmanager for this test', async () => {
|
||||||
|
await smartAcmeInstance.certmanager.wipe();
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('get a domain certificate via DNS-01 challenge', async () => {
|
tap.test('get a domain certificate via DNS-01 challenge', async () => {
|
||||||
// Replace 'bleu.de' with your test domain if different
|
// Replace 'bleu.de' with your test domain if different
|
||||||
const domain = 'bleu.de';
|
const domain = 'bleu.de';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
import { SmartAcme } from '../ts/index.js';
|
import { SmartAcme, MemoryCertManager } from '../ts/index.js';
|
||||||
import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js';
|
import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js';
|
||||||
|
|
||||||
// Dummy handler for testing
|
// Dummy handler for testing
|
||||||
@ -12,7 +12,7 @@ class DummyHandler implements IChallengeHandler<any> {
|
|||||||
tap.test('constructor throws without challengeHandlers', async () => {
|
tap.test('constructor throws without challengeHandlers', async () => {
|
||||||
expect(() => new SmartAcme({
|
expect(() => new SmartAcme({
|
||||||
accountEmail: 'test@example.com',
|
accountEmail: 'test@example.com',
|
||||||
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
|
certManager: new MemoryCertManager(),
|
||||||
environment: 'integration',
|
environment: 'integration',
|
||||||
retryOptions: {},
|
retryOptions: {},
|
||||||
} as any)).toThrow();
|
} as any)).toThrow();
|
||||||
@ -21,7 +21,7 @@ tap.test('constructor throws without challengeHandlers', async () => {
|
|||||||
tap.test('constructor accepts valid challengeHandlers', async () => {
|
tap.test('constructor accepts valid challengeHandlers', async () => {
|
||||||
const sa = new SmartAcme({
|
const sa = new SmartAcme({
|
||||||
accountEmail: 'test@example.com',
|
accountEmail: 'test@example.com',
|
||||||
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
|
certManager: new MemoryCertManager(),
|
||||||
environment: 'integration',
|
environment: 'integration',
|
||||||
retryOptions: {},
|
retryOptions: {},
|
||||||
challengeHandlers: [new DummyHandler()],
|
challengeHandlers: [new DummyHandler()],
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartacme',
|
name: '@push.rocks/smartacme',
|
||||||
version: '6.1.0',
|
version: '7.2.0',
|
||||||
description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
|
description: 'A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.'
|
||||||
}
|
}
|
||||||
|
2
ts/certmanagers/index.ts
Normal file
2
ts/certmanagers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './memory.js';
|
||||||
|
export * from './mongo.js';
|
49
ts/certmanagers/memory.ts
Normal file
49
ts/certmanagers/memory.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import * as plugins from '../smartacme.plugins.js';
|
||||||
|
import type { ICertManager } from '../interfaces/certmanager.js';
|
||||||
|
import { SmartacmeCert } from '../smartacme.classes.cert.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory certificate manager for mongoless mode.
|
||||||
|
* Stores certificates in memory only and does not connect to MongoDB.
|
||||||
|
*/
|
||||||
|
export class MemoryCertManager implements ICertManager {
|
||||||
|
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||||
|
private certs: Map<string, SmartacmeCert> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
// no-op for in-memory store
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
|
||||||
|
return this.certs.get(domainName) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
|
||||||
|
this.certs.set(cert.domainName, cert);
|
||||||
|
const interest = this.interestMap.findInterest(cert.domainName);
|
||||||
|
if (interest) {
|
||||||
|
interest.fullfillInterest(cert);
|
||||||
|
interest.markLost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCertificate(domainName: string): Promise<void> {
|
||||||
|
this.certs.delete(domainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async close(): Promise<void> {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wipe all certificates from the in-memory store (for testing)
|
||||||
|
*/
|
||||||
|
public async wipe(): Promise<void> {
|
||||||
|
this.certs.clear();
|
||||||
|
// reset interest map
|
||||||
|
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||||
|
}
|
||||||
|
}
|
61
ts/certmanagers/mongo.ts
Normal file
61
ts/certmanagers/mongo.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import * as plugins from '../smartacme.plugins.js';
|
||||||
|
import type { ICertManager } from '../interfaces/certmanager.js';
|
||||||
|
import { SmartacmeCert } from '../smartacme.classes.cert.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MongoDB-backed certificate manager using EasyStore from smartdata.
|
||||||
|
*/
|
||||||
|
export class MongoCertManager implements ICertManager {
|
||||||
|
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
||||||
|
private db: plugins.smartdata.SmartdataDb;
|
||||||
|
private store: plugins.smartdata.EasyStore<Record<string, any>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mongoDescriptor MongoDB connection settings
|
||||||
|
*/
|
||||||
|
constructor(mongoDescriptor: plugins.smartdata.IMongoDescriptor) {
|
||||||
|
this.db = new plugins.smartdata.SmartdataDb(mongoDescriptor);
|
||||||
|
// Use a single EasyStore document to hold all certs keyed by domainName
|
||||||
|
this.store = new plugins.smartdata.EasyStore<Record<string, any>>(
|
||||||
|
'smartacme-certs',
|
||||||
|
this.db,
|
||||||
|
);
|
||||||
|
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
await this.db.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
|
||||||
|
const data = await this.store.readKey(domainName);
|
||||||
|
return data ? new SmartacmeCert(data) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
|
||||||
|
// write plain object for persistence
|
||||||
|
await this.store.writeKey(cert.domainName, { ...cert });
|
||||||
|
const interest = this.interestMap.findInterest(cert.domainName);
|
||||||
|
if (interest) {
|
||||||
|
interest.fullfillInterest(cert);
|
||||||
|
interest.markLost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCertificate(domainName: string): Promise<void> {
|
||||||
|
await this.store.deleteKey(domainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async close(): Promise<void> {
|
||||||
|
await this.db.close();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wipe all certificates from the persistent store (for integration testing)
|
||||||
|
*/
|
||||||
|
public async wipe(): Promise<void> {
|
||||||
|
// clear all keys in the easy store
|
||||||
|
await this.store.wipe();
|
||||||
|
// reset interest map
|
||||||
|
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
|
||||||
|
}
|
||||||
|
}
|
@ -5,14 +5,14 @@ import type { IChallengeHandler } from './IChallengeHandler.js';
|
|||||||
* DNS-01 challenge handler using CloudflareAccount and Smartdns.
|
* DNS-01 challenge handler using CloudflareAccount and Smartdns.
|
||||||
*/
|
*/
|
||||||
export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.IDnsChallenge> {
|
export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.IDnsChallenge> {
|
||||||
private cf: any;
|
private cf: plugins.tsclass.network.IConvenientDnsProvider;
|
||||||
private smartdns: plugins.smartdnsClient.Smartdns;
|
private smartdns: plugins.smartdnsClient.Smartdns;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
cloudflareAccount: any,
|
convenientDnsProvider: plugins.tsclass.network.IConvenientDnsProvider,
|
||||||
smartdnsInstance?: plugins.smartdnsClient.Smartdns,
|
smartdnsInstance?: plugins.smartdnsClient.Smartdns,
|
||||||
) {
|
) {
|
||||||
this.cf = cloudflareAccount;
|
this.cf = convenientDnsProvider;
|
||||||
this.smartdns = smartdnsInstance ?? new plugins.smartdnsClient.Smartdns({});
|
this.smartdns = smartdnsInstance ?? new plugins.smartdnsClient.Smartdns({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,18 +23,14 @@ export class Dns01Handler implements IChallengeHandler<plugins.tsclass.network.I
|
|||||||
public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
|
public async prepare(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
|
||||||
// set DNS TXT record
|
// set DNS TXT record
|
||||||
await this.cf.convenience.acmeSetDnsChallenge(ch);
|
await this.cf.convenience.acmeSetDnsChallenge(ch);
|
||||||
// wait for DNS propagation
|
|
||||||
await this.smartdns.checkUntilAvailable(
|
|
||||||
ch.hostName,
|
|
||||||
'TXT',
|
|
||||||
ch.challenge,
|
|
||||||
100,
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
|
public async cleanup(ch: plugins.tsclass.network.IDnsChallenge): Promise<void> {
|
||||||
// remove DNS TXT record
|
// remove DNS TXT record
|
||||||
await this.cf.convenience.acmeRemoveDnsChallenge(ch);
|
await this.cf.convenience.acmeRemoveDnsChallenge(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async checkWetherDomainIsSupported(domainArg: string): Promise<boolean> {
|
||||||
|
return this.cf.convenience.isDomainSupported(domainArg);
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,14 +6,14 @@ import type { IChallengeHandler } from './IChallengeHandler.js';
|
|||||||
* HTTP-01 ACME challenge handler using file-system webroot.
|
* HTTP-01 ACME challenge handler using file-system webroot.
|
||||||
* Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/.
|
* Writes and removes the challenge file under <webroot>/.well-known/acme-challenge/.
|
||||||
*/
|
*/
|
||||||
export interface Http01HandlerOptions {
|
export interface Http01WebrootOptions {
|
||||||
/**
|
/**
|
||||||
* Directory that serves HTTP requests for /.well-known/acme-challenge
|
* Directory that serves HTTP requests for /.well-known/acme-challenge
|
||||||
*/
|
*/
|
||||||
webroot: string;
|
webroot: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Http01Handler implements IChallengeHandler<{
|
export class Http01Webroot implements IChallengeHandler<{
|
||||||
type: string;
|
type: string;
|
||||||
token: string;
|
token: string;
|
||||||
keyAuthorization: string;
|
keyAuthorization: string;
|
||||||
@ -21,7 +21,7 @@ export class Http01Handler implements IChallengeHandler<{
|
|||||||
}> {
|
}> {
|
||||||
private webroot: string;
|
private webroot: string;
|
||||||
|
|
||||||
constructor(options: Http01HandlerOptions) {
|
constructor(options: Http01WebrootOptions) {
|
||||||
this.webroot = options.webroot;
|
this.webroot = options.webroot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
67
ts/handlers/Http01MemoryHandler.ts
Normal file
67
ts/handlers/Http01MemoryHandler.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { IChallengeHandler } from './IChallengeHandler.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP-01 ACME challenge handler using in-memory storage.
|
||||||
|
* Stores challenge tokens and key authorizations in memory
|
||||||
|
* and serves them via handleRequest for arbitrary HTTP servers.
|
||||||
|
*/
|
||||||
|
export interface Http01MemoryHandlerChallenge {
|
||||||
|
type: string;
|
||||||
|
token: string;
|
||||||
|
keyAuthorization: string;
|
||||||
|
webPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Http01MemoryHandler implements IChallengeHandler<Http01MemoryHandlerChallenge> {
|
||||||
|
private store: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
public getSupportedTypes(): string[] {
|
||||||
|
return ['http-01'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async prepare(ch: Http01MemoryHandlerChallenge): Promise<void> {
|
||||||
|
this.store.set(ch.token, ch.keyAuthorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verify(_ch: Http01MemoryHandlerChallenge): Promise<void> {
|
||||||
|
// No-op
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cleanup(ch: Http01MemoryHandlerChallenge): Promise<void> {
|
||||||
|
this.store.delete(ch.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request handler for serving ACME HTTP-01 challenges.
|
||||||
|
* @param req HTTP request object (should have url property)
|
||||||
|
* @param res HTTP response object
|
||||||
|
* @param next Optional next() callback for Express-style fallthrough
|
||||||
|
*/
|
||||||
|
public handleRequest(req: any, res: any, next?: () => void): void {
|
||||||
|
const url = req.url || '';
|
||||||
|
const prefix = '/.well-known/acme-challenge/';
|
||||||
|
if (!url.startsWith(prefix)) {
|
||||||
|
if (next) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
res.statusCode = 404;
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
const token = url.slice(prefix.length);
|
||||||
|
const keyAuth = this.store.get(token);
|
||||||
|
if (keyAuth !== undefined) {
|
||||||
|
if (typeof res.status === 'function' && typeof res.send === 'function') {
|
||||||
|
return res.status(200).send(keyAuth);
|
||||||
|
}
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
return res.end(keyAuth);
|
||||||
|
}
|
||||||
|
if (next) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
res.statusCode = 404;
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
export type { IChallengeHandler } from './IChallengeHandler.js';
|
export type { IChallengeHandler } from './IChallengeHandler.js';
|
||||||
// Removed legacy handler adapter
|
// Removed legacy handler adapter
|
||||||
export { Dns01Handler } from './Dns01Handler.js';
|
export { Dns01Handler } from './Dns01Handler.js';
|
||||||
export { Http01Handler } from './Http01Handler.js';
|
export { Http01Webroot } from './Http01Handler.js';
|
||||||
|
export { Http01MemoryHandler } from './Http01MemoryHandler.js';
|
@ -1,2 +1,4 @@
|
|||||||
export * from './smartacme.classes.smartacme.js';
|
export * from './smartacme.classes.smartacme.js';
|
||||||
export { SmartacmeCert as Cert } from './smartacme.classes.cert.js';
|
export { SmartacmeCert as Cert } from './smartacme.classes.cert.js';
|
||||||
|
export type { ICertManager } from './interfaces/certmanager.js';
|
||||||
|
export { MemoryCertManager, MongoCertManager } from './certmanagers/index.js';
|
||||||
|
36
ts/interfaces/certmanager.ts
Normal file
36
ts/interfaces/certmanager.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { SmartacmeCert } from '../smartacme.classes.cert.js';
|
||||||
|
|
||||||
|
// (ICertRecord removed; use SmartacmeCert directly)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for certificate storage managers.
|
||||||
|
* Users can implement this to provide custom persistence (in-memory,
|
||||||
|
* file-based, Redis, etc.).
|
||||||
|
*/
|
||||||
|
export interface ICertManager {
|
||||||
|
/**
|
||||||
|
* Initialize the store (e.g., connect to database).
|
||||||
|
*/
|
||||||
|
init(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Retrieve a certificate record by domain name.
|
||||||
|
* Returns null if none found.
|
||||||
|
*/
|
||||||
|
retrieveCertificate(domainName: string): Promise<SmartacmeCert | null>;
|
||||||
|
/**
|
||||||
|
* Store a certificate record. Fulfills any pending interests.
|
||||||
|
*/
|
||||||
|
storeCertificate(cert: SmartacmeCert): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Delete a certificate record by domain name.
|
||||||
|
*/
|
||||||
|
deleteCertificate(domainName: string): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Close the store (e.g., disconnect database).
|
||||||
|
*/
|
||||||
|
close(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Optional: wipe all stored certificates (e.g., for integration testing)
|
||||||
|
*/
|
||||||
|
wipe(): Promise<void>;
|
||||||
|
}
|
@ -1,64 +1,39 @@
|
|||||||
import * as plugins from './smartacme.plugins.js';
|
import * as plugins from './smartacme.plugins.js';
|
||||||
|
|
||||||
import * as interfaces from './interfaces/index.js';
|
/**
|
||||||
|
* Plain certificate record.
|
||||||
import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
|
*/
|
||||||
|
export class SmartacmeCert {
|
||||||
import { Collection, svDb, unI } from '@push.rocks/smartdata';
|
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => {
|
|
||||||
return SmartacmeCertManager.activeDB;
|
|
||||||
})
|
|
||||||
export class SmartacmeCert
|
|
||||||
extends plugins.smartdata.SmartDataDbDoc<SmartacmeCert, plugins.tsclass.network.ICert>
|
|
||||||
implements plugins.tsclass.network.ICert
|
|
||||||
{
|
|
||||||
@unI()
|
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@svDb()
|
|
||||||
public domainName: string;
|
public domainName: string;
|
||||||
|
|
||||||
@svDb()
|
|
||||||
public created: number;
|
public created: number;
|
||||||
|
|
||||||
@svDb()
|
|
||||||
public privateKey: string;
|
public privateKey: string;
|
||||||
|
|
||||||
@svDb()
|
|
||||||
public publicKey: string;
|
public publicKey: string;
|
||||||
|
|
||||||
@svDb()
|
|
||||||
public csr: string;
|
public csr: string;
|
||||||
|
|
||||||
@svDb()
|
|
||||||
public validUntil: number;
|
public validUntil: number;
|
||||||
|
|
||||||
|
constructor(data: Partial<SmartacmeCert> = {}) {
|
||||||
|
this.id = data.id || '';
|
||||||
|
this.domainName = data.domainName || '';
|
||||||
|
this.created = data.created || Date.now();
|
||||||
|
this.privateKey = data.privateKey || '';
|
||||||
|
this.publicKey = data.publicKey || '';
|
||||||
|
this.csr = data.csr || '';
|
||||||
|
this.validUntil = data.validUntil || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if certificate is still valid.
|
||||||
|
*/
|
||||||
public isStillValid(): boolean {
|
public isStillValid(): boolean {
|
||||||
return this.validUntil >= Date.now();
|
return this.validUntil >= Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if certificate needs renewal (e.g., expires in <10 days).
|
||||||
|
*/
|
||||||
public shouldBeRenewed(): boolean {
|
public shouldBeRenewed(): boolean {
|
||||||
const shouldBeValidAtLeastUntil =
|
const threshold = Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 10 });
|
||||||
Date.now() +
|
return this.validUntil < threshold;
|
||||||
plugins.smarttime.getMilliSecondsFromUnits({
|
|
||||||
days: 10,
|
|
||||||
});
|
|
||||||
return !(this.validUntil >= shouldBeValidAtLeastUntil);
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(certDataArg: plugins.tsclass.network.ICert) {
|
|
||||||
Object.keys(certDataArg).forEach((key) => {
|
|
||||||
this[key] = certDataArg[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(optionsArg: plugins.tsclass.network.ICert) {
|
|
||||||
super();
|
|
||||||
if (optionsArg) {
|
|
||||||
Object.keys(optionsArg).forEach((key) => {
|
|
||||||
this[key] = optionsArg[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import * as plugins from './smartacme.plugins.js';
|
|
||||||
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
|
||||||
import { SmartAcme } from './smartacme.classes.smartacme.js';
|
|
||||||
|
|
||||||
import * as interfaces from './interfaces/index.js';
|
|
||||||
|
|
||||||
export class SmartacmeCertManager {
|
|
||||||
// =========
|
|
||||||
// STATIC
|
|
||||||
// =========
|
|
||||||
public static activeDB: plugins.smartdata.SmartdataDb;
|
|
||||||
|
|
||||||
// =========
|
|
||||||
// INSTANCE
|
|
||||||
// =========
|
|
||||||
private mongoDescriptor: plugins.smartdata.IMongoDescriptor;
|
|
||||||
public smartdataDb: plugins.smartdata.SmartdataDb;
|
|
||||||
|
|
||||||
public interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
smartAcmeArg: SmartAcme,
|
|
||||||
optionsArg: {
|
|
||||||
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
this.mongoDescriptor = optionsArg.mongoDescriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async init() {
|
|
||||||
// Smartdata DB
|
|
||||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.mongoDescriptor);
|
|
||||||
await this.smartdataDb.init();
|
|
||||||
SmartacmeCertManager.activeDB = this.smartdataDb;
|
|
||||||
|
|
||||||
// Pending Map
|
|
||||||
this.interestMap = new plugins.lik.InterestMap((certName) => certName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieves a certificate
|
|
||||||
* @returns the Cert class or null
|
|
||||||
* @param certDomainNameArg the domain Name to retrieve the vcertificate for
|
|
||||||
*/
|
|
||||||
public async retrieveCertificate(certDomainNameArg: string): Promise<SmartacmeCert> {
|
|
||||||
const existingCertificate: SmartacmeCert = await SmartacmeCert.getInstance<SmartacmeCert>({
|
|
||||||
domainName: certDomainNameArg,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingCertificate) {
|
|
||||||
return existingCertificate;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* stores the certificate
|
|
||||||
* @param optionsArg
|
|
||||||
*/
|
|
||||||
public async storeCertificate(optionsArg: plugins.tsclass.network.ICert) {
|
|
||||||
const cert = new SmartacmeCert(optionsArg);
|
|
||||||
await cert.save();
|
|
||||||
const interest = this.interestMap.findInterest(cert.domainName);
|
|
||||||
if (interest) {
|
|
||||||
interest.fullfillInterest(cert);
|
|
||||||
interest.markLost();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteCertificate(certDomainNameArg: string) {
|
|
||||||
const cert: SmartacmeCert = await SmartacmeCert.getInstance<SmartacmeCert>({
|
|
||||||
domainName: certDomainNameArg,
|
|
||||||
});
|
|
||||||
await cert.delete();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
import * as plugins from './smartacme.plugins.js';
|
import * as plugins from './smartacme.plugins.js';
|
||||||
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
import type { ICertManager } from './interfaces/certmanager.js';
|
||||||
import { SmartacmeCertManager } from './smartacme.classes.certmanager.js';
|
|
||||||
import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
|
import { SmartacmeCertMatcher } from './smartacme.classes.certmatcher.js';
|
||||||
import { commitinfo } from './00_commitinfo_data.js';
|
import { commitinfo } from './00_commitinfo_data.js';
|
||||||
|
import { SmartacmeCert } from './smartacme.classes.cert.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the options for the class @see SmartAcme
|
* the options for the class @see SmartAcme
|
||||||
@ -10,7 +10,10 @@ import { commitinfo } from './00_commitinfo_data.js';
|
|||||||
export interface ISmartAcmeOptions {
|
export interface ISmartAcmeOptions {
|
||||||
accountPrivateKey?: string;
|
accountPrivateKey?: string;
|
||||||
accountEmail: string;
|
accountEmail: string;
|
||||||
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
|
/**
|
||||||
|
* Certificate storage manager (e.g., Mongo or in-memory).
|
||||||
|
*/
|
||||||
|
certManager: ICertManager;
|
||||||
// Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
|
// Removed legacy setChallenge/removeChallenge in favor of `challengeHandlers`
|
||||||
environment: 'production' | 'integration';
|
environment: 'production' | 'integration';
|
||||||
/**
|
/**
|
||||||
@ -59,8 +62,8 @@ export class SmartAcme {
|
|||||||
private privateKey: string;
|
private privateKey: string;
|
||||||
|
|
||||||
|
|
||||||
// certmanager
|
// certificate manager for persistence (implements ICertManager)
|
||||||
private certmanager: SmartacmeCertManager;
|
public certmanager: ICertManager;
|
||||||
private certmatcher: SmartacmeCertMatcher;
|
private certmatcher: SmartacmeCertMatcher;
|
||||||
// retry/backoff configuration (resolved with defaults)
|
// retry/backoff configuration (resolved with defaults)
|
||||||
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
|
private retryOptions: { retries: number; factor: number; minTimeoutMs: number; maxTimeoutMs: number };
|
||||||
@ -78,10 +81,10 @@ export class SmartAcme {
|
|||||||
this.logger.enableConsole();
|
this.logger.enableConsole();
|
||||||
// initialize retry/backoff options
|
// initialize retry/backoff options
|
||||||
this.retryOptions = {
|
this.retryOptions = {
|
||||||
retries: optionsArg.retryOptions?.retries ?? 3,
|
retries: optionsArg.retryOptions?.retries ?? 10,
|
||||||
factor: optionsArg.retryOptions?.factor ?? 2,
|
factor: optionsArg.retryOptions?.factor ?? 4,
|
||||||
minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
|
minTimeoutMs: optionsArg.retryOptions?.minTimeoutMs ?? 1000,
|
||||||
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 30000,
|
maxTimeoutMs: optionsArg.retryOptions?.maxTimeoutMs ?? 60000,
|
||||||
};
|
};
|
||||||
// initialize challenge handlers (must provide at least one)
|
// initialize challenge handlers (must provide at least one)
|
||||||
if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
|
if (!optionsArg.challengeHandlers || optionsArg.challengeHandlers.length === 0) {
|
||||||
@ -107,10 +110,11 @@ export class SmartAcme {
|
|||||||
this.privateKey =
|
this.privateKey =
|
||||||
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
|
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
|
||||||
|
|
||||||
// CertMangaer
|
// Initialize certificate manager
|
||||||
this.certmanager = new SmartacmeCertManager(this, {
|
if (!this.options.certManager) {
|
||||||
mongoDescriptor: this.options.mongoDescriptor,
|
throw new Error('You must provide a certManager via options.certManager');
|
||||||
});
|
}
|
||||||
|
this.certmanager = this.options.certManager;
|
||||||
await this.certmanager.init();
|
await this.certmanager.init();
|
||||||
|
|
||||||
// CertMatcher
|
// CertMatcher
|
||||||
@ -138,9 +142,14 @@ export class SmartAcme {
|
|||||||
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
|
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop() {
|
/**
|
||||||
await this.certmanager.smartdataDb.close();
|
* Stops the SmartAcme instance and closes certificate store connections.
|
||||||
|
*/
|
||||||
|
public async stop() {
|
||||||
|
if (this.certmanager && typeof (this.certmanager as any).close === 'function') {
|
||||||
|
await (this.certmanager as any).close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/** Retry helper with exponential backoff */
|
/** Retry helper with exponential backoff */
|
||||||
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
|
private async retry<T>(operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> {
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
@ -221,7 +230,8 @@ export class SmartAcme {
|
|||||||
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
|
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
|
||||||
return retrievedCertificate;
|
return retrievedCertificate;
|
||||||
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
|
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
|
||||||
await retrievedCertificate.delete();
|
// Remove old certificate via certManager
|
||||||
|
await this.certmanager.deleteCertificate(certDomainName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// lets make sure others get the same interest
|
// lets make sure others get the same interest
|
||||||
@ -277,15 +287,45 @@ export class SmartAcme {
|
|||||||
}
|
}
|
||||||
this.pendingChallenges.push(input);
|
this.pendingChallenges.push(input);
|
||||||
try {
|
try {
|
||||||
|
// Prepare the challenge (set DNS record, write file, etc.)
|
||||||
await this.retry(() => handler.prepare(input), `${type}.prepare`);
|
await this.retry(() => handler.prepare(input), `${type}.prepare`);
|
||||||
if (handler.verify) {
|
// For DNS-01, wait for propagation before verification
|
||||||
await this.retry(() => handler.verify!(input), `${type}.verify`);
|
if (type === 'dns-01') {
|
||||||
} else {
|
const dnsInput = input as { hostName: string; challenge: string };
|
||||||
await this.retry(() => this.client.verifyChallenge(authz, selectedChallengeArg), `${type}.verifyChallenge`);
|
// Wait for authoritative DNS propagation before ACME verify
|
||||||
|
await this.retry(
|
||||||
|
() => this.smartdns.checkUntilAvailable(dnsInput.hostName, 'TXT', dnsInput.challenge, 100, 5000),
|
||||||
|
`${type}.propagation`,
|
||||||
|
);
|
||||||
|
// Extra cool-down to ensure ACME server sees the new TXT record
|
||||||
|
this.logger.log('info', 'Cooling down for 1 minute before ACME verification');
|
||||||
|
await plugins.smartdelay.delayFor(60000);
|
||||||
|
}
|
||||||
|
// Official ACME verification (ensures challenge is publicly reachable)
|
||||||
|
await this.retry(
|
||||||
|
() => this.client.verifyChallenge(authz, selectedChallengeArg),
|
||||||
|
`${type}.verifyChallenge`,
|
||||||
|
);
|
||||||
|
// Notify ACME server to complete the challenge
|
||||||
|
await this.retry(
|
||||||
|
() => this.client.completeChallenge(selectedChallengeArg),
|
||||||
|
`${type}.completeChallenge`,
|
||||||
|
);
|
||||||
|
// Wait for valid status (warnings on staging timeouts)
|
||||||
|
try {
|
||||||
|
await this.retry(
|
||||||
|
() => this.client.waitForValidStatus(selectedChallengeArg),
|
||||||
|
`${type}.waitForValidStatus`,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
await this.logger.log(
|
||||||
|
'warn',
|
||||||
|
`Challenge ${type} did not reach valid status in time, proceeding to finalize`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await this.retry(() => this.client.completeChallenge(selectedChallengeArg), `${type}.completeChallenge`);
|
|
||||||
await this.retry(() => this.client.waitForValidStatus(selectedChallengeArg), `${type}.waitForValidStatus`);
|
|
||||||
} finally {
|
} finally {
|
||||||
|
// Always cleanup resource
|
||||||
try {
|
try {
|
||||||
await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
|
await this.retry(() => handler.cleanup(input), `${type}.cleanup`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -307,19 +347,17 @@ export class SmartAcme {
|
|||||||
|
|
||||||
/* Done */
|
/* Done */
|
||||||
|
|
||||||
await this.certmanager.storeCertificate({
|
// Store the new certificate record
|
||||||
|
const certRecord = new SmartacmeCert({
|
||||||
id: plugins.smartunique.shortId(),
|
id: plugins.smartunique.shortId(),
|
||||||
domainName: certDomainName,
|
domainName: certDomainName,
|
||||||
privateKey: key.toString(),
|
privateKey: key.toString(),
|
||||||
publicKey: cert.toString(),
|
publicKey: cert.toString(),
|
||||||
csr: csr.toString(),
|
csr: csr.toString(),
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
validUntil:
|
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
|
||||||
Date.now() +
|
|
||||||
plugins.smarttime.getMilliSecondsFromUnits({
|
|
||||||
days: 90,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
await this.certmanager.storeCertificate(certRecord);
|
||||||
|
|
||||||
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
||||||
currentDomainInterst.fullfillInterest(newCertificate);
|
currentDomainInterst.fullfillInterest(newCertificate);
|
||||||
@ -327,7 +365,4 @@ export class SmartAcme {
|
|||||||
return newCertificate;
|
return newCertificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllCertificates(): Promise<SmartacmeCert[]> {
|
|
||||||
return SmartacmeCert.getInstances({});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
// @apiclient.xyz scope
|
||||||
|
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||||
|
|
||||||
|
export { cloudflare };
|
||||||
|
|
||||||
// @apiglobal scope
|
// @apiglobal scope
|
||||||
import * as typedserver from '@api.global/typedserver';
|
import * as typedserver from '@api.global/typedserver';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user