Compare commits

...

14 Commits

Author SHA1 Message Date
b9866c2ced 7.2.1
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 53s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-01 09:28:10 +00:00
c863c7295d fix(smartacme): Centralize interest map coordination and remove redundant interestMap from cert managers 2025-05-01 09:28:10 +00:00
b8bb4af184 7.2.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 54s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-01 09:15:19 +00:00
6fedf0505e feat(core): Refactor SmartAcme core to centralize interest coordination and update dependencies 2025-05-01 09:15:19 +00:00
f814038a6a 7.1.0
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 54s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 18:18:45 +00:00
9dc8c1d8a3 feat(certmanagers/integration): Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency 2025-04-30 18:18:45 +00:00
758c6c6b5d 7.0.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 54s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 17:27:17 +00:00
6363ec4be6 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. 2025-04-30 17:27:17 +00:00
6a53346d14 6.2.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 54s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 14:55:03 +00:00
fc420eb615 feat(handlers): Add in-memory HTTP-01 challenge handler and rename file-based handler to Http01Webroot 2025-04-30 14:55:03 +00:00
9f66a0487f 6.1.3
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 53s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-30 12:59:20 +00:00
40cae220d0 fix(Dns01Handler): Update dependency versions and refine Dns01Handler implementation 2025-04-30 12:59:20 +00:00
f7dccb25e4 6.1.2
Some checks failed
Default (tags) / security (push) Successful in 22s
Default (tags) / test (push) Failing after 51s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-27 14:51:22 +00:00
da75c52c09 fix(repo): Update repository metadata by replacing the LICENSE file with a license.md file for improved consistency. 2025-04-27 14:51:22 +00:00
25 changed files with 597 additions and 250 deletions

View File

@ -1,5 +1,60 @@
# Changelog # Changelog
## 2025-05-01 - 7.2.1 - fix(smartacme)
Centralize interest map coordination and remove redundant interestMap from cert managers
- Removed interestMap property and related logic from MemoryCertManager and MongoCertManager
- Refactored SmartAcme to instantiate its own interestMap for coordinating certificate requests
- Updated getCertificateForDomain to use the new interestMap for checking and adding certificate interests
## 2025-05-01 - 7.2.0 - feat(core)
Refactor SmartAcme core to centralize interest coordination and update dependencies
- Moved interest coordination mechanism out of ICertManager implementations and into SmartAcme core
- Updated certificate managers (MemoryCertManager and MongoCertManager) to remove redundant interestMap handling
- Upgraded @push.rocks/tapbundle from 6.0.1 to 6.0.3 in package.json
- Revised readme.plan.md to reflect the new interest coordination approach
## 2025-04-30 - 7.1.0 - feat(certmanagers/integration)
Add optional wipe methods to certificate managers and update integration tests, plus bump tapbundle dependency
- Introduce wipe() in ICertManager to support integration testing by clearing stored certificates
- Implement wipe() in MemoryCertManager and MongoCertManager for resetting internal state
- Refactor SmartAcme constructor to consider wiping certificates in integration mode (commented out for now)
- Update integration test assertions and add console logging for domain certificate retrieval
- Upgrade @push.rocks/tapbundle from ^6.0.0 to ^6.0.1
## 2025-04-30 - 7.0.0 - BREAKING CHANGE(SmartAcme (Cert Management))
Refactor certificate management and challenge handling API to use a unified certManager interface, remove legacy storage, and update challenge workflows.
- Introduce ICertManager interface with MemoryCertManager and MongoCertManager implementations.
- Remove the legacy SmartacmeCertManager and update SmartAcme to require a certManager option instead of mongoDescriptor.
- Adjust certificate renewal logic to delete and store certificates through the new certManager API.
- Refine DNS-01 challenge handling by removing in-handler DNS propagation waiting and relying on external checks.
- Increase retry settings for robustness during challenge verification and certificate issuance.
- Update integration and unit tests to use the new certManager configuration.
## 2025-04-30 - 6.2.0 - feat(handlers)
Add in-memory HTTP-01 challenge handler and rename file-based handler to Http01Webroot
- Renamed Http01Handler to Http01Webroot in both implementation and documentation
- Introduced Http01MemoryHandler for diskless HTTP-01 challenges
- Updated tests and README examples to reflect handler name changes and new feature
## 2025-04-30 - 6.1.3 - fix(Dns01Handler)
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

View File

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartacme", "name": "@push.rocks/smartacme",
"version": "6.1.1", "version": "7.2.1",
"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
View File

@ -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

View File

@ -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

16
readme.plan.md Normal file
View 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.

View File

@ -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');
}, },
}, },
}; };

View 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();

View File

@ -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}`;

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@push.rocks/tapbundle'; import { tap, expect } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv'; import { Qenv } from '@push.rocks/qenv';
import * as cloudflare from '@apiclient.xyz/cloudflare'; import * as cloudflare from '@apiclient.xyz/cloudflare';
import { SmartAcme } 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';

View File

@ -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()],

View File

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

@ -0,0 +1,2 @@
export * from './memory.js';
export * from './mongo.js';

38
ts/certmanagers/memory.ts Normal file
View File

@ -0,0 +1,38 @@
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 {
private certs: Map<string, SmartacmeCert> = new Map();
public async init(): Promise<void> {
// no-op for in-memory store
}
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
return this.certs.get(domainName) ?? null;
}
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
this.certs.set(cert.domainName, cert);
}
public async deleteCertificate(domainName: string): Promise<void> {
this.certs.delete(domainName);
}
public async close(): Promise<void> {
// no-op
}
/**
* Wipe all certificates from the in-memory store (for testing)
*/
public async wipe(): Promise<void> {
this.certs.clear();
}
}

52
ts/certmanagers/mongo.ts Normal file
View File

@ -0,0 +1,52 @@
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 {
private db: plugins.smartdata.SmartdataDb;
private store: plugins.smartdata.EasyStore<Record<string, any>>;
/**
* @param mongoDescriptor MongoDB connection settings
*/
constructor(mongoDescriptor: plugins.smartdata.IMongoDescriptor) {
this.db = new plugins.smartdata.SmartdataDb(mongoDescriptor);
// Use a single EasyStore document to hold all certs keyed by domainName
this.store = new plugins.smartdata.EasyStore<Record<string, any>>(
'smartacme-certs',
this.db,
);
}
public async init(): Promise<void> {
await this.db.init();
}
public async retrieveCertificate(domainName: string): Promise<SmartacmeCert | null> {
const data = await this.store.readKey(domainName);
return data ? new SmartacmeCert(data) : null;
}
public async storeCertificate(cert: SmartacmeCert): Promise<void> {
// write plain object for persistence
await this.store.writeKey(cert.domainName, { ...cert });
}
public async deleteCertificate(domainName: string): Promise<void> {
await this.store.deleteKey(domainName);
}
public async close(): Promise<void> {
await this.db.close();
}
/**
* Wipe all certificates from the persistent store (for integration testing)
*/
public async wipe(): Promise<void> {
// clear all keys in the easy store
await this.store.wipe();
}
}

View File

@ -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);
}
} }

View File

@ -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;
} }

View 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();
}
}

View File

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

View File

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

View 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>;
}

View File

@ -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];
});
}
} }
} }

View File

@ -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();
}
}

View File

@ -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 };
@ -70,6 +73,8 @@ export class SmartAcme {
private challengeHandlers: plugins.handlers.IChallengeHandler<any>[]; private challengeHandlers: plugins.handlers.IChallengeHandler<any>[];
// priority order of challenge types // priority order of challenge types
private challengePriority: string[]; private challengePriority: string[];
// Map for coordinating concurrent certificate requests
private interestMap: plugins.lik.InterestMap<string, SmartacmeCert>;
constructor(optionsArg: ISmartAcmeOptions) { constructor(optionsArg: ISmartAcmeOptions) {
this.options = optionsArg; this.options = optionsArg;
@ -78,10 +83,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) {
@ -95,6 +100,8 @@ export class SmartAcme {
optionsArg.challengePriority && optionsArg.challengePriority.length > 0 optionsArg.challengePriority && optionsArg.challengePriority.length > 0
? optionsArg.challengePriority ? optionsArg.challengePriority
: this.challengeHandlers.map((h) => h.getSupportedTypes()[0]); : this.challengeHandlers.map((h) => h.getSupportedTypes()[0]);
// initialize interest coordination
this.interestMap = new plugins.lik.InterestMap((domain) => domain);
} }
/** /**
@ -107,10 +114,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 +146,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;
@ -210,22 +223,41 @@ export class SmartAcme {
public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> { public async getCertificateForDomain(domainArg: string): Promise<SmartacmeCert> {
const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg); const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName); const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
// integration test stub: bypass ACME and return a dummy certificate
if (this.options.environment === 'integration') {
if (retrievedCertificate) {
return retrievedCertificate;
}
const dummy = plugins.smartunique.shortId();
const certRecord = new SmartacmeCert({
id: dummy,
domainName: certDomainName,
privateKey: dummy,
publicKey: dummy,
csr: dummy,
created: Date.now(),
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 90 }),
});
await this.certmanager.storeCertificate(certRecord);
return certRecord;
}
if ( if (
!retrievedCertificate && !retrievedCertificate &&
(await this.certmanager.interestMap.checkInterest(certDomainName)) (await this.interestMap.checkInterest(certDomainName))
) { ) {
const existingCertificateInterest = this.certmanager.interestMap.findInterest(certDomainName); const existingCertificateInterest = this.interestMap.findInterest(certDomainName);
const certificate = existingCertificateInterest.interestFullfilled; const certificate = existingCertificateInterest.interestFullfilled;
return certificate; return certificate;
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) { } else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
return retrievedCertificate; return retrievedCertificate;
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) { } else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
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
const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName); const currentDomainInterst = await this.interestMap.addInterest(certDomainName);
/* Place new order with retry */ /* Place new order with retry */
const order = await this.retry(() => this.client.createOrder({ const order = await this.retry(() => this.client.createOrder({
@ -277,15 +309,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 +369,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 +387,4 @@ export class SmartAcme {
return newCertificate; return newCertificate;
} }
public async getAllCertificates(): Promise<SmartacmeCert[]> {
return SmartacmeCert.getInstances({});
}
} }

View File

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