Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
758c6c6b5d | |||
6363ec4be6 | |||
6a53346d14 | |||
fc420eb615 | |||
9f66a0487f | |||
40cae220d0 | |||
f7dccb25e4 | |||
da75c52c09 |
31
changelog.md
31
changelog.md
@ -1,5 +1,36 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
## 2025-04-27 - 6.1.1 - fix(readme)
|
||||||
Fix license link reference in documentation
|
Fix license link reference in documentation
|
||||||
|
|
||||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartacme",
|
"name": "@push.rocks/smartacme",
|
||||||
"version": "6.1.1",
|
"version": "7.0.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.0",
|
||||||
"@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.0
|
||||||
version: 5.6.3(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
|
version: 6.0.0(@aws-sdk/credential-providers@3.797.0)(socks@2.8.4)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.15.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.2.2':
|
||||||
|
resolution: {integrity: sha512-s2zJlLc6Wub7P/jgKSM51kW2UjslxQwx2BXoyJVO95OgiOwarde0AuxPR0lfRA/FvHdBfTmJf4upiWtcjYMB/Q==}
|
||||||
|
|
||||||
'@push.rocks/smartfeed@1.0.11':
|
'@push.rocks/smartfeed@1.0.11':
|
||||||
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
|
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
|
||||||
|
|
||||||
@ -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.0':
|
||||||
|
resolution: {integrity: sha512-ARIs189TysvI8EsPAC7LH6O0WbBYI9E7XxdihwmM6LRgLvzAbp1agfO6lOjpKrAYWKjT3KdlUEihilxOBrgTYQ==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
'@push.rocks/taskbuffer@3.1.7':
|
||||||
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
||||||
|
|
||||||
@ -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.2.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.0(@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.2.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
|
||||||
|
25
readme.md
25
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
|
||||||
|
|
||||||
|
44
readme.plan.md
Normal file
44
readme.plan.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Plan: Diskless HTTP-01 Handler and Renaming Existing Handler
|
||||||
|
|
||||||
|
This plan outlines steps to rename the existing filesystem-based HTTP-01 handler to `Http01Webroot`
|
||||||
|
and introduce a new diskless (in-memory) HTTP-01 handler for integration with arbitrary HTTP servers
|
||||||
|
(e.g., Express).
|
||||||
|
|
||||||
|
## 1. Rename existing handler to Http01Webroot
|
||||||
|
- In `ts/handlers/Http01Handler.ts`:
|
||||||
|
- Rename `Http01HandlerOptions` to `Http01WebrootOptions`.
|
||||||
|
- Rename class `Http01Handler` to `Http01Webroot`.
|
||||||
|
- Remove the legacy alias; rename the handler directly.
|
||||||
|
- In `ts/handlers/index.ts`:
|
||||||
|
- Export `Http01Webroot` under its new name.
|
||||||
|
- Remove any `Http01Handler` export.
|
||||||
|
- Update existing tests (e.g., `test.handlers-http01.ts`) to import `Http01Webroot` instead of `Http01Handler`.
|
||||||
|
|
||||||
|
## 2. Add new diskless (in-memory) HTTP-01 handler
|
||||||
|
- Create `ts/handlers/Http01MemoryHandler.ts`:
|
||||||
|
- Implement `IChallengeHandler<{ token: string; keyAuthorization: string; webPath: string }>`, storing challenges in a private `Map<string, string>`.
|
||||||
|
- `prepare()`: add token→keyAuthorization mapping.
|
||||||
|
- `verify()`: no-op.
|
||||||
|
- `cleanup()`: remove mapping.
|
||||||
|
- Add `handleRequest(req, res, next?)` method:
|
||||||
|
- Parse `/.well-known/acme-challenge/:token` from `req.url`.
|
||||||
|
- If token exists, respond with the key authorization and status 200.
|
||||||
|
- If missing and `next` provided, call `next()`, otherwise respond 404.
|
||||||
|
- Export `Http01MemoryHandler` in `ts/handlers/index.ts`.
|
||||||
|
|
||||||
|
## 3. Write tests for Http01MemoryHandler
|
||||||
|
- Create `test/test.handlers-http01-memory.ts`:
|
||||||
|
- Use `tap` and `expect` to:
|
||||||
|
1. `prepare()` a challenge.
|
||||||
|
2. Invoke `handleRequest()` with a fake `req`/`res` to confirm 200 and correct body.
|
||||||
|
3. `cleanup()` the challenge.
|
||||||
|
4. Confirm `handleRequest()` now yields 404.
|
||||||
|
|
||||||
|
## 4. Update documentation
|
||||||
|
- Add examples in `readme.md` showing how to use both `Http01Webroot` and the new `Http01MemoryHandler`:
|
||||||
|
- Sample code for Express integration using `handleRequest`.
|
||||||
|
|
||||||
|
## 5. Build and test
|
||||||
|
- Run `pnpm build` and `pnpm test`, ensuring existing tests are updated for `Http01Webroot` and new tests pass.
|
||||||
|
|
||||||
|
Please review and let me know if this plan makes sense before proceeding with implementation.
|
@ -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/)
|
||||||
@ -19,7 +19,7 @@ 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)],
|
||||||
@ -34,7 +34,7 @@ tap.test('get a domain certificate via DNS-01 challenge', async () => {
|
|||||||
const domain = 'bleu.de';
|
const domain = 'bleu.de';
|
||||||
const cert = await smartAcmeInstance.getCertificateForDomain(domain);
|
const cert = await smartAcmeInstance.getCertificateForDomain(domain);
|
||||||
expect(cert).toHaveProperty('domainName');
|
expect(cert).toHaveProperty('domainName');
|
||||||
expect(cert.domainName).toEqual(domain);
|
expect(cert).toEqual(domain);
|
||||||
expect(cert).toHaveProperty('publicKey');
|
expect(cert).toHaveProperty('publicKey');
|
||||||
expect(typeof cert.publicKey).toEqual('string');
|
expect(typeof cert.publicKey).toEqual('string');
|
||||||
expect(cert.publicKey.length).toBeGreaterThan(0);
|
expect(cert.publicKey.length).toBeGreaterThan(0);
|
||||||
|
@ -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.1',
|
version: '7.0.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.'
|
||||||
}
|
}
|
||||||
|
90
ts/certmanagers.ts
Normal file
90
ts/certmanagers.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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.js';
|
||||||
|
37
ts/interfaces/certmanager.ts
Normal file
37
ts/interfaces/certmanager.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { InterestMap } from '@push.rocks/lik';
|
||||||
|
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 {
|
||||||
|
/**
|
||||||
|
* Map for coordinating concurrent certificate requests.
|
||||||
|
*/
|
||||||
|
interestMap: InterestMap<string, SmartacmeCert>;
|
||||||
|
/**
|
||||||
|
* 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>;
|
||||||
|
}
|
@ -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;
|
private 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,8 +142,13 @@ export class SmartAcme {
|
|||||||
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
|
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the SmartAcme instance and closes certificate store connections.
|
||||||
|
*/
|
||||||
public async stop() {
|
public async stop() {
|
||||||
await this.certmanager.smartdataDb.close();
|
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> {
|
||||||
@ -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