Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
edd8ca8d70 | |||
8a396a04fa | |||
09aadc702e | |||
a59ebd6202 | |||
0d8740d812 | |||
e6a138279d |
18
changelog.md
18
changelog.md
@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-02 - 8.0.0 - BREAKING CHANGE(certProvisioner)
|
||||
Refactor: Introduce unified CertProvisioner to centralize certificate provisioning and renewal; remove legacy ACME config from Port80Handler and update SmartProxy to delegate certificate lifecycle management.
|
||||
|
||||
- Removed deprecated acme properties and renewal scheduler from IPort80HandlerOptions and Port80Handler.
|
||||
- Created new CertProvisioner component in ts/smartproxy/classes.pp.certprovisioner.ts to handle static and HTTP-01 certificate workflows.
|
||||
- Updated SmartProxy to initialize CertProvisioner and re-emit certificate events.
|
||||
- Eliminated legacy renewal logic and associated autoRenew settings from Port80Handler.
|
||||
- Adjusted tests to reflect changes in certificate provisioning and renewal behavior.
|
||||
|
||||
## 2025-05-01 - 7.2.0 - feat(ACME/Certificate)
|
||||
Introduce certificate provider hook and observable certificate events; remove legacy ACME flow
|
||||
|
||||
- Extended IPortProxySettings with a new certProvider callback that allows returning a static certificate or 'http01' for ACME challenges.
|
||||
- Updated Port80Handler to leverage SmartAcme's getCertificateForDomain and removed outdated methods such as getAcmeClient and processAuthorizations.
|
||||
- Enhanced SmartProxy to extend EventEmitter, invoking certProvider on non-wildcard domains and re-emitting certificate events (with domain, publicKey, privateKey, expiryDate, source, and isRenewal flag).
|
||||
- Updated NetworkProxyBridge to support applying external certificates via a new applyExternalCertificate method.
|
||||
- Revised documentation (readme.md and readme.plan.md) to include usage examples for the new certificate provider hook.
|
||||
|
||||
## 2025-04-30 - 7.1.4 - fix(dependencies)
|
||||
Update dependency versions in package.json
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "7.1.4",
|
||||
"version": "8.0.0",
|
||||
"private": false,
|
||||
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
|
||||
"main": "dist_ts/index.js",
|
||||
@ -18,21 +18,22 @@
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.77",
|
||||
"@push.rocks/tapbundle": "^6.0.0",
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^22.15.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartacme": "^7.2.3",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartnetwork": "^4.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@tsclass/tsclass": "^9.1.0",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"acme-client": "^5.4.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"ws": "^8.18.1"
|
||||
|
422
pnpm-lock.yaml
generated
422
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
||||
'@push.rocks/lik':
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^7.2.3
|
||||
version: 7.2.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@push.rocks/smartdelay':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
@ -26,6 +29,9 @@ importers:
|
||||
'@push.rocks/smartstring':
|
||||
specifier: ^4.0.15
|
||||
version: 4.0.15
|
||||
'@push.rocks/taskbuffer':
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0
|
||||
@ -35,9 +41,6 @@ importers:
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
acme-client:
|
||||
specifier: ^5.4.0
|
||||
version: 5.4.0
|
||||
minimatch:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
@ -58,8 +61,8 @@ importers:
|
||||
specifier: ^1.0.77
|
||||
version: 1.0.96(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)(typescript@5.8.3)
|
||||
'@push.rocks/tapbundle':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@types/node':
|
||||
specifier: ^22.15.3
|
||||
version: 22.15.3
|
||||
@ -81,9 +84,15 @@ packages:
|
||||
'@api.global/typedserver@3.0.68':
|
||||
resolution: {integrity: sha512-7o6fkz60ed8q2lmEe44hsu/6kNqG4j5WVgWwmY+a1MmSOUtuu5+VTYYNyc8KrSgtbRBzx4+2A2N31l4wDjcy3w==}
|
||||
|
||||
'@api.global/typedserver@3.0.74':
|
||||
resolution: {integrity: sha512-lrXaCPaVZLihlF9w39pEqTw2kiHFCheRKTZuK07S7gTGyfdXKPmccVR/EK4ox58E1gjh9A2K8yY8ZWGcjuSJkw==}
|
||||
|
||||
'@api.global/typedsocket@3.0.1':
|
||||
resolution: {integrity: sha512-xojiAVNXtHoxkpBo8U2HHJG8FrVXXuLvDNndSHXwx4C9VslUwDn5zSCI+PdBl8iAg+ZuBmKjqkpZZ9sL6DC5yQ==}
|
||||
|
||||
'@apiclient.xyz/cloudflare@6.4.1':
|
||||
resolution: {integrity: sha512-RYFphnbunjK+Imq/3ynIQpAvIGBJ38kqSZ2nrpTm26zsBIxW7S6xEe3zhXfVMtUIgC99OL3Xr/SGXl3CNBwCug==}
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -346,6 +355,9 @@ packages:
|
||||
'@cloudflare/workers-types@4.20250303.0':
|
||||
resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==}
|
||||
|
||||
'@cloudflare/workers-types@4.20250430.0':
|
||||
resolution: {integrity: sha512-JWAX7ZhQ7KjkdJwASgG58MZ/pQ15brlnZ9/0YBwDQ0hrJ/LaK392aTRFlj2r/PRKDZ5dOuujRywNYaNpfeFiEA==}
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
@ -724,6 +736,9 @@ packages:
|
||||
'@lit/reactive-element@2.0.4':
|
||||
resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==}
|
||||
|
||||
'@lit/reactive-element@2.1.0':
|
||||
resolution: {integrity: sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==}
|
||||
|
||||
'@mixmark-io/domino@2.2.0':
|
||||
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
|
||||
|
||||
@ -857,6 +872,9 @@ packages:
|
||||
'@push.rocks/qenv@6.1.0':
|
||||
resolution: {integrity: sha512-1FUFMlSVwFSFg8LbqfkzJ2LLP4lMGApUtgOpsvrde6+AxBmB4gjoNgCUH7z3xXfDAtYqcrtSELXBNE0xVL1MqQ==}
|
||||
|
||||
'@push.rocks/smartacme@7.2.3':
|
||||
resolution: {integrity: sha512-PTwn/Zf7l+IMWqeiQ8mTxi7fdrtObQH13YzF65si/VxXTqHeZ7zvisLLKZcMEgSaOj1aQ/Ku83gaO8YqO4gDig==}
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
|
||||
|
||||
@ -887,6 +905,9 @@ packages:
|
||||
'@push.rocks/smartdelay@3.0.5':
|
||||
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
||||
|
||||
'@push.rocks/smartdns@6.2.2':
|
||||
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
|
||||
|
||||
'@push.rocks/smartenv@5.0.12':
|
||||
resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==}
|
||||
|
||||
@ -896,8 +917,8 @@ packages:
|
||||
'@push.rocks/smartexpect@1.6.1':
|
||||
resolution: {integrity: sha512-NFQXEPkGiMNxyvFwKyzDWe3ADYdf8KNvIcV7TGNZZT3uPQtk65te4Q+a1cWErjP/61yE9XdYiQA66QQp+TV9IQ==}
|
||||
|
||||
'@push.rocks/smartexpect@2.2.2':
|
||||
resolution: {integrity: sha512-s2zJlLc6Wub7P/jgKSM51kW2UjslxQwx2BXoyJVO95OgiOwarde0AuxPR0lfRA/FvHdBfTmJf4upiWtcjYMB/Q==}
|
||||
'@push.rocks/smartexpect@2.4.2':
|
||||
resolution: {integrity: sha512-L+aS1n5rWhf/yOh5R3zPgwycYtDr5FfrDWgasy6ShhN6Zbn/z/AOPbWcF/OpeTmx0XabWB2h5d4xBcCKLl47cQ==}
|
||||
|
||||
'@push.rocks/smartfeed@1.0.11':
|
||||
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
|
||||
@ -1043,8 +1064,8 @@ packages:
|
||||
'@push.rocks/tapbundle@5.6.3':
|
||||
resolution: {integrity: sha512-hFzsf59rg1K70i45llj7PCyyCZp7JW19XRR+Q1gge1T0pBN8Wi53aYqP/2qtxdMiNVe2s3ESp6VJZv3sLOMYPQ==}
|
||||
|
||||
'@push.rocks/tapbundle@6.0.0':
|
||||
resolution: {integrity: sha512-ARIs189TysvI8EsPAC7LH6O0WbBYI9E7XxdihwmM6LRgLvzAbp1agfO6lOjpKrAYWKjT3KdlUEihilxOBrgTYQ==}
|
||||
'@push.rocks/tapbundle@6.0.3':
|
||||
resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==}
|
||||
|
||||
'@push.rocks/taskbuffer@3.1.7':
|
||||
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
||||
@ -1540,6 +1561,9 @@ packages:
|
||||
'@tsclass/tsclass@4.4.4':
|
||||
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
|
||||
|
||||
'@tsclass/tsclass@5.0.0':
|
||||
resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
|
||||
|
||||
'@tsclass/tsclass@8.2.1':
|
||||
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
|
||||
|
||||
@ -1552,6 +1576,9 @@ packages:
|
||||
'@types/babel__code-frame@7.0.6':
|
||||
resolution: {integrity: sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==}
|
||||
|
||||
'@types/bn.js@5.1.6':
|
||||
resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==}
|
||||
|
||||
'@types/body-parser@1.19.5':
|
||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||
|
||||
@ -1606,6 +1633,12 @@ packages:
|
||||
'@types/default-gateway@7.2.2':
|
||||
resolution: {integrity: sha512-35C93fYQlnLKLASkMPoxRvok4fENwB3By9clRLd2I/08n/XRl0pCdf7EB17K5oMMwZu8NBYA8i66jH5r/LYBKA==}
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==}
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
|
||||
|
||||
'@types/express-serve-static-core@4.19.6':
|
||||
resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==}
|
||||
|
||||
@ -1618,6 +1651,9 @@ packages:
|
||||
'@types/express@5.0.0':
|
||||
resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==}
|
||||
|
||||
'@types/express@5.0.1':
|
||||
resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==}
|
||||
|
||||
'@types/fast-json-stable-stringify@2.1.2':
|
||||
resolution: {integrity: sha512-vsxcbfLDdjytnCnHXtinE40Xl46Wr7l/VGRGt7ewJwCPMKEHOdEsTxXX8xwgoR7cbc+6dE8SB4jlMrOV2zAg7g==}
|
||||
deprecated: This is a stub types definition. fast-json-stable-stringify provides its own type definitions, so you do not need this installed.
|
||||
@ -1700,9 +1736,15 @@ packages:
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
||||
|
||||
'@types/node@18.19.87':
|
||||
resolution: {integrity: sha512-OIAAu6ypnVZHmsHCeJ+7CCSub38QNBS9uceMQeg7K5Ur0Jr+wG9wEOEvvMbhp09pxD5czIUy/jND7s7Tb6Nw7A==}
|
||||
|
||||
'@types/node@22.15.3':
|
||||
resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==}
|
||||
|
||||
@ -1825,6 +1867,10 @@ packages:
|
||||
resolution: {integrity: sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
|
||||
accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -1890,8 +1936,8 @@ packages:
|
||||
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
asn1js@3.0.5:
|
||||
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
|
||||
asn1js@3.0.6:
|
||||
resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
ast-types@0.13.4:
|
||||
@ -1915,8 +1961,8 @@ packages:
|
||||
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axios@1.8.2:
|
||||
resolution: {integrity: sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==}
|
||||
axios@1.9.0:
|
||||
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
|
||||
|
||||
b4a@1.6.7:
|
||||
resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
|
||||
@ -1963,6 +2009,9 @@ packages:
|
||||
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
bn.js@4.12.2:
|
||||
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
|
||||
|
||||
body-parser@1.20.3:
|
||||
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
@ -1983,6 +2032,9 @@ packages:
|
||||
broadcast-channel@7.0.0:
|
||||
resolution: {integrity: sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==}
|
||||
|
||||
brorand@1.1.0:
|
||||
resolution: {integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=}
|
||||
|
||||
browserify-zlib@0.1.4:
|
||||
resolution: {integrity: sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=}
|
||||
|
||||
@ -2120,6 +2172,9 @@ packages:
|
||||
resolution: {integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
cloudflare@4.2.0:
|
||||
resolution: {integrity: sha512-L9IainZq+7PN3NG/Mg7T9oRm7SB7AskNe6QLD8j7FrJY+2Dn1qpeLqXF8Im04ANnssezf0KAOtSuxNAAyNEgkg==}
|
||||
|
||||
co-body@6.2.0:
|
||||
resolution: {integrity: sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@ -2389,6 +2444,9 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
||||
|
||||
elliptic@6.6.1:
|
||||
resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@ -2515,6 +2573,10 @@ packages:
|
||||
resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
@ -2635,6 +2697,9 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
|
||||
form-data-encoder@2.1.4:
|
||||
resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==}
|
||||
engines: {node: '>= 14.17'}
|
||||
@ -2647,6 +2712,10 @@ packages:
|
||||
resolution: {integrity: sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
||||
formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -2785,6 +2854,9 @@ packages:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hash.js@1.1.7:
|
||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2802,6 +2874,9 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
@ -3148,12 +3223,21 @@ packages:
|
||||
lit-element@4.1.1:
|
||||
resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==}
|
||||
|
||||
lit-element@4.2.0:
|
||||
resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==}
|
||||
|
||||
lit-html@3.2.1:
|
||||
resolution: {integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==}
|
||||
|
||||
lit-html@3.3.0:
|
||||
resolution: {integrity: sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==}
|
||||
|
||||
lit@3.2.1:
|
||||
resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==}
|
||||
|
||||
lit@3.3.0:
|
||||
resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
@ -3447,6 +3531,12 @@ packages:
|
||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
|
||||
minimalistic-crypto-utils@1.0.1:
|
||||
resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
|
||||
|
||||
minimatch@10.0.1:
|
||||
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@ -3564,6 +3654,19 @@ packages:
|
||||
no-case@2.3.2:
|
||||
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-forge@1.3.1:
|
||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
@ -4265,6 +4368,9 @@ packages:
|
||||
resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=}
|
||||
|
||||
tr46@3.0.0:
|
||||
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
|
||||
engines: {node: '>=12'}
|
||||
@ -4302,8 +4408,8 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tsyringe@4.8.0:
|
||||
resolution: {integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==}
|
||||
tsyringe@4.10.0:
|
||||
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
turndown-plugin-gfm@1.0.2:
|
||||
@ -4354,6 +4460,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
@ -4418,6 +4527,13 @@ packages:
|
||||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
@ -4434,6 +4550,9 @@ packages:
|
||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -4636,6 +4755,53 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@api.global/typedserver@3.0.74':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 3.0.1
|
||||
'@cloudflare/workers-types': 4.20250430.0
|
||||
'@design.estate/dees-comms': 1.0.27
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartchok': 1.0.34
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartenv': 5.0.12
|
||||
'@push.rocks/smartfeed': 1.0.11
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartjson': 5.0.20
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
'@push.rocks/smartmanifest': 2.0.2
|
||||
'@push.rocks/smartmatch': 2.0.0
|
||||
'@push.rocks/smartmime': 2.0.4
|
||||
'@push.rocks/smartntml': 2.0.8
|
||||
'@push.rocks/smartopen': 2.0.0
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartsitemap': 2.0.3
|
||||
'@push.rocks/smartstream': 3.2.5
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@push.rocks/taskbuffer': 3.1.7
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
'@push.rocks/webstore': 2.0.20
|
||||
'@tsclass/tsclass': 8.2.1
|
||||
'@types/express': 5.0.1
|
||||
body-parser: 1.20.3
|
||||
cors: 2.8.5
|
||||
express: 4.21.2
|
||||
express-force-ssl: 0.3.2
|
||||
lit: 3.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- bufferutil
|
||||
- react
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@api.global/typedsocket@3.0.1':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
@ -4654,6 +4820,18 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@apiclient.xyz/cloudflare@6.4.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
'@tsclass/tsclass': 9.1.0
|
||||
cloudflare: 4.2.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
@ -5493,6 +5671,8 @@ snapshots:
|
||||
|
||||
'@cloudflare/workers-types@4.20250303.0': {}
|
||||
|
||||
'@cloudflare/workers-types@4.20250430.0': {}
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
@ -5839,6 +6019,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@lit-labs/ssr-dom-shim': 1.3.0
|
||||
|
||||
'@lit/reactive-element@2.1.0':
|
||||
dependencies:
|
||||
'@lit-labs/ssr-dom-shim': 1.3.0
|
||||
|
||||
'@mixmark-io/domino@2.2.0': {}
|
||||
|
||||
'@mongodb-js/saslprep@1.2.2':
|
||||
@ -5922,21 +6106,21 @@ snapshots:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
'@peculiar/asn1-x509': 2.3.15
|
||||
'@peculiar/asn1-x509-attr': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-csr@2.3.15':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
'@peculiar/asn1-x509': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-ecc@2.3.15':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
'@peculiar/asn1-x509': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pfx@2.3.15':
|
||||
@ -5945,14 +6129,14 @@ snapshots:
|
||||
'@peculiar/asn1-pkcs8': 2.3.15
|
||||
'@peculiar/asn1-rsa': 2.3.15
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pkcs8@2.3.15':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
'@peculiar/asn1-x509': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pkcs9@2.3.15':
|
||||
@ -5963,19 +6147,19 @@ snapshots:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
'@peculiar/asn1-x509': 2.3.15
|
||||
'@peculiar/asn1-x509-attr': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-rsa@2.3.15':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
'@peculiar/asn1-x509': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-schema@2.3.15':
|
||||
dependencies:
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
pvtsutils: 1.3.6
|
||||
tslib: 2.8.1
|
||||
|
||||
@ -5983,13 +6167,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
'@peculiar/asn1-x509': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-x509@2.3.15':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.15
|
||||
asn1js: 3.0.5
|
||||
asn1js: 3.0.6
|
||||
pvtsutils: 1.3.6
|
||||
tslib: 2.8.1
|
||||
|
||||
@ -6005,7 +6189,7 @@ snapshots:
|
||||
pvtsutils: 1.3.6
|
||||
reflect-metadata: 0.2.2
|
||||
tslib: 2.8.1
|
||||
tsyringe: 4.8.0
|
||||
tsyringe: 4.10.0
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
@ -6100,6 +6284,38 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
|
||||
'@push.rocks/smartacme@7.2.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.74
|
||||
'@apiclient.xyz/cloudflare': 6.4.1
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdata': 5.15.1(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartdns': 6.2.2
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
'@tsclass/tsclass': 9.1.0
|
||||
acme-client: 5.4.0
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
- mongodb-client-encryption
|
||||
- react
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
dependencies:
|
||||
'@push.rocks/smartfile': 10.0.41
|
||||
@ -6205,6 +6421,22 @@ snapshots:
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
|
||||
'@push.rocks/smartdns@6.2.2':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartenv': 5.0.12
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@tsclass/tsclass': 5.0.0
|
||||
'@types/dns-packet': 5.6.5
|
||||
'@types/elliptic': 6.4.18
|
||||
acme-client: 5.4.0
|
||||
dns-packet: 5.6.1
|
||||
elliptic: 6.6.1
|
||||
minimatch: 10.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartenv@5.0.12':
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -6222,7 +6454,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
fast-deep-equal: 3.1.3
|
||||
|
||||
'@push.rocks/smartexpect@2.2.2':
|
||||
'@push.rocks/smartexpect@2.4.2':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -6652,7 +6884,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@push.rocks/tapbundle@6.0.0(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
'@push.rocks/tapbundle@6.0.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
dependencies:
|
||||
'@open-wc/testing': 4.0.0
|
||||
'@push.rocks/consolecolor': 2.0.2
|
||||
@ -6660,7 +6892,7 @@ snapshots:
|
||||
'@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/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.798.0)(socks@2.8.4)
|
||||
@ -6690,7 +6922,7 @@ snapshots:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.7
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
|
||||
@ -7495,6 +7727,10 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 4.37.0
|
||||
|
||||
'@tsclass/tsclass@5.0.0':
|
||||
dependencies:
|
||||
type-fest: 4.40.1
|
||||
|
||||
'@tsclass/tsclass@8.2.1':
|
||||
dependencies:
|
||||
type-fest: 4.40.1
|
||||
@ -7509,6 +7745,10 @@ snapshots:
|
||||
|
||||
'@types/babel__code-frame@7.0.6': {}
|
||||
|
||||
'@types/bn.js@5.1.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
|
||||
'@types/body-parser@1.19.5':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
@ -7569,6 +7809,14 @@ snapshots:
|
||||
|
||||
'@types/default-gateway@7.2.2': {}
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
'@types/bn.js': 5.1.6
|
||||
|
||||
'@types/express-serve-static-core@4.19.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
@ -7597,6 +7845,12 @@ snapshots:
|
||||
'@types/qs': 6.9.18
|
||||
'@types/serve-static': 1.15.7
|
||||
|
||||
'@types/express@5.0.1':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.5
|
||||
'@types/express-serve-static-core': 5.0.6
|
||||
'@types/serve-static': 1.15.7
|
||||
|
||||
'@types/fast-json-stable-stringify@2.1.2':
|
||||
dependencies:
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
@ -7693,10 +7947,19 @@ snapshots:
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.3
|
||||
|
||||
'@types/node@18.19.87':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@22.15.3':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@ -7877,6 +8140,10 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
|
||||
accepts@1.3.8:
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
@ -7885,8 +8152,8 @@ snapshots:
|
||||
acme-client@5.4.0:
|
||||
dependencies:
|
||||
'@peculiar/x509': 1.12.3
|
||||
asn1js: 3.0.5
|
||||
axios: 1.8.2(debug@4.4.0)
|
||||
asn1js: 3.0.6
|
||||
axios: 1.9.0(debug@4.4.0)
|
||||
debug: 4.4.0
|
||||
node-forge: 1.3.1
|
||||
transitivePeerDependencies:
|
||||
@ -7935,7 +8202,7 @@ snapshots:
|
||||
|
||||
array-union@2.1.0: {}
|
||||
|
||||
asn1js@3.0.5:
|
||||
asn1js@3.0.6:
|
||||
dependencies:
|
||||
pvtsutils: 1.3.6
|
||||
pvutils: 1.1.3
|
||||
@ -7957,7 +8224,7 @@ snapshots:
|
||||
|
||||
axe-core@4.10.3: {}
|
||||
|
||||
axios@1.8.2(debug@4.4.0):
|
||||
axios@1.9.0(debug@4.4.0):
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9(debug@4.4.0)
|
||||
form-data: 4.0.2
|
||||
@ -8004,6 +8271,8 @@ snapshots:
|
||||
|
||||
basic-ftp@5.0.5: {}
|
||||
|
||||
bn.js@4.12.2: {}
|
||||
|
||||
body-parser@1.20.3:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@ -8043,6 +8312,8 @@ snapshots:
|
||||
p-queue: 6.6.2
|
||||
unload: 2.4.1
|
||||
|
||||
brorand@1.1.0: {}
|
||||
|
||||
browserify-zlib@0.1.4:
|
||||
dependencies:
|
||||
pako: 0.2.9
|
||||
@ -8181,6 +8452,18 @@ snapshots:
|
||||
|
||||
clone@2.1.2: {}
|
||||
|
||||
cloudflare@4.2.0:
|
||||
dependencies:
|
||||
'@types/node': 18.19.87
|
||||
'@types/node-fetch': 2.6.12
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
co-body@6.2.0:
|
||||
dependencies:
|
||||
'@hapi/bourne': 3.0.0
|
||||
@ -8407,6 +8690,16 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
elliptic@6.6.1:
|
||||
dependencies:
|
||||
bn.js: 4.12.2
|
||||
brorand: 1.1.0
|
||||
hash.js: 1.1.7
|
||||
hmac-drbg: 1.0.1
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
minimalistic-crypto-utils: 1.0.1
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
@ -8567,6 +8860,8 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
execa@5.1.1:
|
||||
@ -8743,6 +9038,8 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data-encoder@1.7.2: {}
|
||||
|
||||
form-data-encoder@2.1.4: {}
|
||||
|
||||
form-data@4.0.2:
|
||||
@ -8754,6 +9051,11 @@ snapshots:
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
formdata-node@4.4.1:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@0.5.2: {}
|
||||
@ -8940,6 +9242,11 @@ snapshots:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hash.js@1.1.7:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@ -8970,6 +9277,12 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
dependencies:
|
||||
hash.js: 1.1.7
|
||||
minimalistic-assert: 1.0.1
|
||||
minimalistic-crypto-utils: 1.0.1
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
html-minifier@4.0.0:
|
||||
@ -9341,16 +9654,32 @@ snapshots:
|
||||
'@lit/reactive-element': 2.0.4
|
||||
lit-html: 3.2.1
|
||||
|
||||
lit-element@4.2.0:
|
||||
dependencies:
|
||||
'@lit-labs/ssr-dom-shim': 1.3.0
|
||||
'@lit/reactive-element': 2.1.0
|
||||
lit-html: 3.3.0
|
||||
|
||||
lit-html@3.2.1:
|
||||
dependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
lit-html@3.3.0:
|
||||
dependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
lit@3.2.1:
|
||||
dependencies:
|
||||
'@lit/reactive-element': 2.0.4
|
||||
lit-element: 4.1.1
|
||||
lit-html: 3.2.1
|
||||
|
||||
lit@3.3.0:
|
||||
dependencies:
|
||||
'@lit/reactive-element': 2.1.0
|
||||
lit-element: 4.2.0
|
||||
lit-html: 3.3.0
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
@ -9812,6 +10141,10 @@ snapshots:
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
minimalistic-assert@1.0.1: {}
|
||||
|
||||
minimalistic-crypto-utils@1.0.1: {}
|
||||
|
||||
minimatch@10.0.1:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
@ -9935,6 +10268,12 @@ snapshots:
|
||||
dependencies:
|
||||
lower-case: 1.1.4
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-forge@1.3.1: {}
|
||||
|
||||
normalize-newline@4.1.0:
|
||||
@ -10753,6 +11092,8 @@ snapshots:
|
||||
'@tokenizer/token': 0.3.0
|
||||
ieee754: 1.2.1
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tr46@3.0.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
@ -10782,7 +11123,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
tsyringe@4.8.0:
|
||||
tsyringe@4.10.0:
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
|
||||
@ -10815,6 +11156,8 @@ snapshots:
|
||||
|
||||
uint8array-extras@1.4.0: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
@ -10883,6 +11226,10 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.2
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
whatwg-mimetype@3.0.0: {}
|
||||
@ -10897,6 +11244,11 @@ snapshots:
|
||||
tr46: 5.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
44
readme.md
44
readme.md
@ -199,6 +199,50 @@ sequenceDiagram
|
||||
- **IP Filtering** - Control access with IP allow/block lists using glob patterns
|
||||
- **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
|
||||
|
||||
## Certificate Provider Hook & Events
|
||||
|
||||
You can customize how certificates are provisioned per domain by using the `certProvider` callback and listen for certificate events emitted by `SmartProxy`.
|
||||
|
||||
```typescript
|
||||
import { SmartProxy } from '@push.rocks/smartproxy';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Example certProvider: static for a specific domain, HTTP-01 otherwise
|
||||
const certProvider = async (domain: string) => {
|
||||
if (domain === 'static.example.com') {
|
||||
// Load from disk or vault
|
||||
return {
|
||||
id: 'static-cert',
|
||||
domainName: domain,
|
||||
created: Date.now(),
|
||||
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000,
|
||||
privateKey: fs.readFileSync('/etc/ssl/private/static.key', 'utf8'),
|
||||
publicKey: fs.readFileSync('/etc/ssl/certs/static.crt', 'utf8'),
|
||||
csr: ''
|
||||
};
|
||||
}
|
||||
// Fallback to ACME HTTP-01 challenge
|
||||
return 'http01';
|
||||
};
|
||||
|
||||
const proxy = new SmartProxy({
|
||||
fromPort: 80,
|
||||
toPort: 8080,
|
||||
domainConfigs: [{
|
||||
domains: ['static.example.com', 'dynamic.example.com'],
|
||||
allowedIPs: ['*']
|
||||
}],
|
||||
certProvider
|
||||
});
|
||||
|
||||
// Listen for certificate issuance or renewal
|
||||
proxy.on('certificate', (evt) => {
|
||||
console.log(`Certificate for ${evt.domain} ready, expires on ${evt.expiryDate}`);
|
||||
});
|
||||
|
||||
await proxy.start();
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### backendProtocol
|
||||
|
@ -0,0 +1,47 @@
|
||||
## Refactor: Introduce a Unified CertProvisioner for Certificate Lifecycle
|
||||
|
||||
- [x] Ensure Port80Handler is challenge-only:
|
||||
- Remove any internal scheduling and deprecated ACME flows (`getAcmeClient`, `processAuthorizations`, `handleAcmeChallenge`) from Port80Handler.
|
||||
- Remove legacy ACME options (`renewThresholdDays`, `renewCheckIntervalHours`, `mongoDescriptor`, etc.) from `IPort80HandlerOptions`.
|
||||
- Retain only methods for HTTP-01 challenge and direct renewals (`obtainCertificate`, `renewCertificate`, `getDomainCertificateStatus`).
|
||||
- [x] Clean up deprecated `acme` configuration:
|
||||
- Remove the `acme` property from `IPortProxySettings` and all legacy references in code.
|
||||
|
||||
- [x] Implement `CertProvisioner` component:
|
||||
- [x] Create class `ts/smartproxy/classes.pp.certprovisioner.ts`.
|
||||
- [x] Constructor accepts:
|
||||
* `domainConfigs: IDomainConfig[]`
|
||||
* `port80Handler: Port80Handler`
|
||||
* `networkProxyBridge: NetworkProxyBridge`
|
||||
* optional `certProvider: (domain) => Promise<ICert | 'http01'>`
|
||||
* `renewThresholdDays`, `renewCheckIntervalHours`, `autoRenew` settings.
|
||||
- Responsibilities:
|
||||
* Initial provisioning: static vs HTTP-01.
|
||||
* Subscribe to Port80Handler events (CERTIFICATE_ISSUED/RENEWED) and to static cert updates.
|
||||
* Re-emit unified `'certificate'` events to SmartProxy.
|
||||
* Central scheduling of renewals via `@push.rocks/taskbuffer`.
|
||||
|
||||
- [x] Refactor SmartProxy:
|
||||
- [x] Remove existing scheduling / renewal logic.
|
||||
- [x] Instantiate `CertProvisioner` in `start()`, delegate cert workflows entirely.
|
||||
- [x] Forward CertProvisioner events to SmartProxy’s `'certificate'` listener.
|
||||
|
||||
- [x] CertProvisioner lifecycle methods:
|
||||
- [x] `start()`: provision all domains, start scheduler.
|
||||
- [x] `stop()`: stop scheduler.
|
||||
- [x] `requestCertificate(domain)`: on-demand provisioning.
|
||||
|
||||
- [x] Handle static certificate auto-refresh:
|
||||
- [x] In the renewal scheduler, for domains with static certs, re-call `certProvider(domain)` near expiry.
|
||||
- [x] Apply returned cert via `networkProxyBridge.applyExternalCertificate()`.
|
||||
|
||||
- [ ] Tests:
|
||||
- Unit tests for `CertProvisioner`, mocking Port80Handler and `certProvider`:
|
||||
* Validate initial provisioning and dynamic/static flows.
|
||||
* Validate scheduling triggers correct renewals.
|
||||
- Integration tests:
|
||||
* Use actual in-memory Port80Handler with short intervals to verify renewals and event emission.
|
||||
|
||||
- [ ] Documentation:
|
||||
- Add code-level TS doc for `CertProvisioner` API (options, methods, events).
|
||||
- Update root `README.md` and architecture diagrams to show `CertProvisioner` role.
|
||||
|
140
test/test.certprovisioner.unit.ts
Normal file
140
test/test.certprovisioner.unit.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import { CertProvisioner } from '../ts/smartproxy/classes.pp.certprovisioner.js';
|
||||
import type { IDomainConfig, ISmartProxyCertProvisionObject } from '../ts/smartproxy/classes.pp.interfaces.js';
|
||||
import type { ICertificateData } from '../ts/port80handler/classes.port80handler.js';
|
||||
|
||||
// Fake Port80Handler stub
|
||||
class FakePort80Handler extends plugins.EventEmitter {
|
||||
public domainsAdded: string[] = [];
|
||||
public renewCalled: string[] = [];
|
||||
addDomain(opts: { domainName: string; sslRedirect: boolean; acmeMaintenance: boolean }) {
|
||||
this.domainsAdded.push(opts.domainName);
|
||||
}
|
||||
async renewCertificate(domain: string): Promise<void> {
|
||||
this.renewCalled.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
// Fake NetworkProxyBridge stub
|
||||
class FakeNetworkProxyBridge {
|
||||
public appliedCerts: ICertificateData[] = [];
|
||||
applyExternalCertificate(cert: ICertificateData) {
|
||||
this.appliedCerts.push(cert);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('CertProvisioner handles static provisioning', async () => {
|
||||
const domain = 'static.com';
|
||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
||||
const fakePort80 = new FakePort80Handler();
|
||||
const fakeBridge = new FakeNetworkProxyBridge();
|
||||
// certProvider returns static certificate
|
||||
const certProvider = async (d: string): Promise<ISmartProxyCertProvisionObject> => {
|
||||
expect(d).toEqual(domain);
|
||||
return {
|
||||
domainName: domain,
|
||||
publicKey: 'CERT',
|
||||
privateKey: 'KEY',
|
||||
validUntil: Date.now() + 3600 * 1000
|
||||
};
|
||||
};
|
||||
const prov = new CertProvisioner(
|
||||
domainConfigs,
|
||||
fakePort80 as any,
|
||||
fakeBridge as any,
|
||||
certProvider,
|
||||
1, // low renew threshold
|
||||
1, // short interval
|
||||
false // disable auto renew for unit test
|
||||
);
|
||||
const events: any[] = [];
|
||||
prov.on('certificate', (data) => events.push(data));
|
||||
await prov.start();
|
||||
// Static flow: no addDomain, certificate applied via bridge
|
||||
expect(fakePort80.domainsAdded.length).toEqual(0);
|
||||
expect(fakeBridge.appliedCerts.length).toEqual(1);
|
||||
expect(events.length).toEqual(1);
|
||||
const evt = events[0];
|
||||
expect(evt.domain).toEqual(domain);
|
||||
expect(evt.certificate).toEqual('CERT');
|
||||
expect(evt.privateKey).toEqual('KEY');
|
||||
expect(evt.isRenewal).toEqual(false);
|
||||
expect(evt.source).toEqual('static');
|
||||
});
|
||||
|
||||
tap.test('CertProvisioner handles http01 provisioning', async () => {
|
||||
const domain = 'http01.com';
|
||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
||||
const fakePort80 = new FakePort80Handler();
|
||||
const fakeBridge = new FakeNetworkProxyBridge();
|
||||
// certProvider returns http01 directive
|
||||
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => 'http01';
|
||||
const prov = new CertProvisioner(
|
||||
domainConfigs,
|
||||
fakePort80 as any,
|
||||
fakeBridge as any,
|
||||
certProvider,
|
||||
1,
|
||||
1,
|
||||
false
|
||||
);
|
||||
const events: any[] = [];
|
||||
prov.on('certificate', (data) => events.push(data));
|
||||
await prov.start();
|
||||
// HTTP-01 flow: addDomain called, no static cert applied
|
||||
expect(fakePort80.domainsAdded).toEqual([domain]);
|
||||
expect(fakeBridge.appliedCerts.length).toEqual(0);
|
||||
expect(events.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
||||
const domain = 'renew.com';
|
||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
||||
const fakePort80 = new FakePort80Handler();
|
||||
const fakeBridge = new FakeNetworkProxyBridge();
|
||||
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => 'http01';
|
||||
const prov = new CertProvisioner(
|
||||
domainConfigs,
|
||||
fakePort80 as any,
|
||||
fakeBridge as any,
|
||||
certProvider,
|
||||
1,
|
||||
1,
|
||||
false
|
||||
);
|
||||
// requestCertificate should call renewCertificate
|
||||
await prov.requestCertificate(domain);
|
||||
expect(fakePort80.renewCalled).toEqual([domain]);
|
||||
});
|
||||
|
||||
tap.test('CertProvisioner on-demand static provisioning', async () => {
|
||||
const domain = 'ondemand.com';
|
||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
||||
const fakePort80 = new FakePort80Handler();
|
||||
const fakeBridge = new FakeNetworkProxyBridge();
|
||||
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => ({
|
||||
domainName: domain,
|
||||
publicKey: 'PKEY',
|
||||
privateKey: 'PRIV',
|
||||
validUntil: Date.now() + 1000
|
||||
});
|
||||
const prov = new CertProvisioner(
|
||||
domainConfigs,
|
||||
fakePort80 as any,
|
||||
fakeBridge as any,
|
||||
certProvider,
|
||||
1,
|
||||
1,
|
||||
false
|
||||
);
|
||||
const events: any[] = [];
|
||||
prov.on('certificate', (data) => events.push(data));
|
||||
await prov.requestCertificate(domain);
|
||||
expect(fakeBridge.appliedCerts.length).toEqual(1);
|
||||
expect(events.length).toEqual(1);
|
||||
expect(events[0].domain).toEqual(domain);
|
||||
expect(events[0].source).toEqual('static');
|
||||
});
|
||||
|
||||
export default tap.start();
|
45
test/test.smartproxy.renewals.node.ts
Normal file
45
test/test.smartproxy.renewals.node.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { SmartProxy } from '../ts/smartproxy/classes.smartproxy.js';
|
||||
|
||||
tap.test('performRenewals only renews domains below threshold', async () => {
|
||||
// Set up SmartProxy instance without real servers
|
||||
const proxy = new SmartProxy({
|
||||
fromPort: 0,
|
||||
toPort: 0,
|
||||
domainConfigs: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: [],
|
||||
globalPortRanges: []
|
||||
});
|
||||
// Stub port80Handler status and renewal
|
||||
const statuses = new Map<string, any>();
|
||||
const now = new Date();
|
||||
statuses.set('expiring.com', {
|
||||
certObtained: true,
|
||||
expiryDate: new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000),
|
||||
obtainingInProgress: false
|
||||
});
|
||||
statuses.set('ok.com', {
|
||||
certObtained: true,
|
||||
expiryDate: new Date(now.getTime() + 100 * 24 * 60 * 60 * 1000),
|
||||
obtainingInProgress: false
|
||||
});
|
||||
const renewed: string[] = [];
|
||||
// Inject fake handler
|
||||
(proxy as any).port80Handler = {
|
||||
getDomainCertificateStatus: () => statuses,
|
||||
renewCertificate: async (domain: string) => { renewed.push(domain); }
|
||||
};
|
||||
// Configure threshold
|
||||
proxy.settings.port80HandlerConfig.enabled = true;
|
||||
proxy.settings.port80HandlerConfig.autoRenew = true;
|
||||
proxy.settings.port80HandlerConfig.renewThresholdDays = 10;
|
||||
|
||||
// Execute renewals
|
||||
await (proxy as any).performRenewals();
|
||||
|
||||
// Only the expiring.com domain should be renewed
|
||||
expect(renewed).toEqual(['expiring.com']);
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '7.1.4',
|
||||
version: '8.0.0',
|
||||
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
@ -353,11 +353,8 @@ export class CertificateManager {
|
||||
port: this.options.acme.port,
|
||||
contactEmail: this.options.acme.contactEmail,
|
||||
useProduction: this.options.acme.useProduction,
|
||||
renewThresholdDays: this.options.acme.renewThresholdDays,
|
||||
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
|
||||
renewCheckIntervalHours: 24, // Check daily for renewals
|
||||
enabled: this.options.acme.enabled,
|
||||
autoRenew: this.options.acme.autoRenew,
|
||||
certificateStore: this.options.acme.certificateStore,
|
||||
skipConfiguredCerts: this.options.acme.skipConfiguredCerts
|
||||
});
|
||||
|
@ -12,6 +12,10 @@ import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
||||
* automatic certificate management, and high-performance connection pooling.
|
||||
*/
|
||||
export class NetworkProxy implements IMetricsTracker {
|
||||
// Provide a minimal JSON representation to avoid circular references during deep equality checks
|
||||
public toJSON(): any {
|
||||
return {};
|
||||
}
|
||||
// Configuration
|
||||
public options: INetworkProxyOptions;
|
||||
public proxyConfigs: IReverseProxyConfig[] = [];
|
||||
|
@ -7,7 +7,6 @@ import * as tls from 'tls';
|
||||
import * as url from 'url';
|
||||
import * as http2 from 'http2';
|
||||
|
||||
|
||||
export { EventEmitter, http, https, net, tls, url, http2 };
|
||||
|
||||
// tsclass scope
|
||||
@ -22,13 +21,27 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartstring from '@push.rocks/smartstring';
|
||||
|
||||
export { lik, smartdelay, smartrequest, smartpromise, smartstring };
|
||||
import * as smartacme from '@push.rocks/smartacme';
|
||||
import * as smartacmePlugins from '@push.rocks/smartacme/dist_ts/smartacme.plugins.js';
|
||||
import * as smartacmeHandlers from '@push.rocks/smartacme/dist_ts/handlers/index.js';
|
||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
|
||||
export {
|
||||
lik,
|
||||
smartdelay,
|
||||
smartrequest,
|
||||
smartpromise,
|
||||
smartstring,
|
||||
smartacme,
|
||||
smartacmePlugins,
|
||||
smartacmeHandlers,
|
||||
taskbuffer,
|
||||
};
|
||||
|
||||
// third party scope
|
||||
import * as acme from 'acme-client';
|
||||
import prettyMs from 'pretty-ms';
|
||||
import * as ws from 'ws';
|
||||
import wsDefault from 'ws';
|
||||
import { minimatch } from 'minimatch';
|
||||
|
||||
export { acme, prettyMs, ws, wsDefault, minimatch };
|
||||
export { prettyMs, ws, wsDefault, minimatch };
|
||||
|
@ -1,7 +1,21 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
// (fs and path I/O moved to CertProvisioner)
|
||||
// ACME HTTP-01 challenge handler storing tokens in memory (diskless)
|
||||
class DisklessHttp01Handler {
|
||||
private storage: Map<string, string>;
|
||||
constructor(storage: Map<string, string>) { this.storage = storage; }
|
||||
public getSupportedTypes(): string[] { return ['http-01']; }
|
||||
public async prepare(ch: any): Promise<void> {
|
||||
this.storage.set(ch.token, ch.keyAuthorization);
|
||||
}
|
||||
public async verify(ch: any): Promise<void> {
|
||||
return;
|
||||
}
|
||||
public async cleanup(ch: any): Promise<void> {
|
||||
this.storage.delete(ch.token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error classes for better error handling
|
||||
@ -59,8 +73,6 @@ interface IDomainCertificate {
|
||||
obtainingInProgress: boolean;
|
||||
certificate?: string;
|
||||
privateKey?: string;
|
||||
challengeToken?: string;
|
||||
challengeKeyAuthorization?: string;
|
||||
expiryDate?: Date;
|
||||
lastRenewalAttempt?: Date;
|
||||
}
|
||||
@ -72,13 +84,9 @@ interface IPort80HandlerOptions {
|
||||
port?: number;
|
||||
contactEmail?: string;
|
||||
useProduction?: boolean;
|
||||
renewThresholdDays?: number;
|
||||
httpsRedirectPort?: number;
|
||||
renewCheckIntervalHours?: number;
|
||||
enabled?: boolean; // Whether ACME is enabled at all
|
||||
autoRenew?: boolean; // Whether to automatically renew certificates
|
||||
certificateStore?: string; // Directory to store certificates
|
||||
skipConfiguredCerts?: boolean; // Skip domains that already have certificates
|
||||
// (Persistence moved to CertProvisioner)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,10 +136,13 @@ export interface ICertificateExpiring {
|
||||
*/
|
||||
export class Port80Handler extends plugins.EventEmitter {
|
||||
private domainCertificates: Map<string, IDomainCertificate>;
|
||||
// In-memory storage for ACME HTTP-01 challenge tokens
|
||||
private acmeHttp01Storage: Map<string, string> = new Map();
|
||||
// SmartAcme instance for certificate management
|
||||
private smartAcme: plugins.smartacme.SmartAcme | null = null;
|
||||
private server: plugins.http.Server | null = null;
|
||||
private acmeClient: plugins.acme.Client | null = null;
|
||||
private accountKey: string | null = null;
|
||||
private renewalTimer: NodeJS.Timeout | null = null;
|
||||
// Renewal scheduling is handled externally by SmartProxy
|
||||
// (Removed internal renewal timer)
|
||||
private isShuttingDown: boolean = false;
|
||||
private options: Required<IPort80HandlerOptions>;
|
||||
|
||||
@ -148,13 +159,8 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
port: options.port ?? 80,
|
||||
contactEmail: options.contactEmail ?? 'admin@example.com',
|
||||
useProduction: options.useProduction ?? false, // Safer default: staging
|
||||
renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
|
||||
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
||||
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
|
||||
enabled: options.enabled ?? true, // Enable by default
|
||||
autoRenew: options.autoRenew ?? true, // Auto-renew by default
|
||||
certificateStore: options.certificateStore ?? './certs', // Default store location
|
||||
skipConfiguredCerts: options.skipConfiguredCerts ?? false
|
||||
enabled: options.enabled ?? true // Enable by default
|
||||
};
|
||||
}
|
||||
|
||||
@ -175,13 +181,20 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
console.log('Port80Handler is disabled, skipping start');
|
||||
return;
|
||||
}
|
||||
// Initialize SmartAcme for ACME challenge management (diskless HTTP handler)
|
||||
if (this.options.enabled) {
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: this.options.contactEmail,
|
||||
certManager: new plugins.smartacme.MemoryCertManager(),
|
||||
environment: this.options.useProduction ? 'production' : 'integration',
|
||||
challengeHandlers: [ new DisklessHttp01Handler(this.acmeHttp01Storage) ],
|
||||
challengePriority: ['http-01'],
|
||||
});
|
||||
await this.smartAcme.start();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Load certificates from store if enabled
|
||||
if (this.options.certificateStore) {
|
||||
this.loadCertificatesFromStore();
|
||||
}
|
||||
|
||||
this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
|
||||
|
||||
@ -197,7 +210,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
this.server.listen(this.options.port, () => {
|
||||
console.log(`Port80Handler is listening on port ${this.options.port}`);
|
||||
this.startRenewalTimer();
|
||||
this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
|
||||
|
||||
// Start certificate process for domains with acmeMaintenance enabled
|
||||
@ -234,11 +246,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// Stop the renewal timer
|
||||
if (this.renewalTimer) {
|
||||
clearInterval(this.renewalTimer);
|
||||
this.renewalTimer = null;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.server) {
|
||||
@ -353,10 +360,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
console.log(`Certificate set for ${domain}`);
|
||||
|
||||
// Save certificate to store if enabled
|
||||
if (this.options.certificateStore) {
|
||||
this.saveCertificateToStore(domain, certificate, privateKey);
|
||||
}
|
||||
// (Persistence of certificates moved to CertProvisioner)
|
||||
|
||||
// Emit certificate event
|
||||
this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
|
||||
@ -391,134 +395,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a certificate to the filesystem store
|
||||
* @param domain The domain for the certificate
|
||||
* @param certificate The certificate (PEM format)
|
||||
* @param privateKey The private key (PEM format)
|
||||
* @private
|
||||
*/
|
||||
private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
|
||||
// Skip if certificate store is not enabled
|
||||
if (!this.options.certificateStore) return;
|
||||
|
||||
try {
|
||||
const storePath = this.options.certificateStore;
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!fs.existsSync(storePath)) {
|
||||
fs.mkdirSync(storePath, { recursive: true });
|
||||
console.log(`Created certificate store directory: ${storePath}`);
|
||||
}
|
||||
|
||||
const certPath = path.join(storePath, `${domain}.cert.pem`);
|
||||
const keyPath = path.join(storePath, `${domain}.key.pem`);
|
||||
|
||||
// Write certificate and private key files
|
||||
fs.writeFileSync(certPath, certificate);
|
||||
fs.writeFileSync(keyPath, privateKey);
|
||||
|
||||
// Set secure permissions for private key
|
||||
try {
|
||||
fs.chmodSync(keyPath, 0o600);
|
||||
} catch (err) {
|
||||
console.log(`Warning: Could not set secure permissions on ${keyPath}`);
|
||||
}
|
||||
|
||||
console.log(`Saved certificate for ${domain} to ${certPath}`);
|
||||
} catch (err) {
|
||||
console.error(`Error saving certificate for ${domain}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads certificates from the certificate store
|
||||
* @private
|
||||
*/
|
||||
private loadCertificatesFromStore(): void {
|
||||
if (!this.options.certificateStore) return;
|
||||
|
||||
try {
|
||||
const storePath = this.options.certificateStore;
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!fs.existsSync(storePath)) {
|
||||
fs.mkdirSync(storePath, { recursive: true });
|
||||
console.log(`Created certificate store directory: ${storePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get list of certificate files
|
||||
const files = fs.readdirSync(storePath);
|
||||
const certFiles = files.filter(file => file.endsWith('.cert.pem'));
|
||||
|
||||
// Load each certificate
|
||||
for (const certFile of certFiles) {
|
||||
const domain = certFile.replace('.cert.pem', '');
|
||||
const keyFile = `${domain}.key.pem`;
|
||||
|
||||
// Skip if key file doesn't exist
|
||||
if (!files.includes(keyFile)) {
|
||||
console.log(`Warning: Found certificate for ${domain} but no key file`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we should skip configured certs
|
||||
if (this.options.skipConfiguredCerts) {
|
||||
const domainInfo = this.domainCertificates.get(domain);
|
||||
if (domainInfo && domainInfo.certObtained) {
|
||||
console.log(`Skipping already configured certificate for ${domain}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Load certificate and key
|
||||
try {
|
||||
const certificate = fs.readFileSync(path.join(storePath, certFile), 'utf8');
|
||||
const privateKey = fs.readFileSync(path.join(storePath, keyFile), 'utf8');
|
||||
|
||||
// Extract expiry date
|
||||
let expiryDate: Date | undefined;
|
||||
try {
|
||||
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
||||
if (matches && matches[1]) {
|
||||
expiryDate = new Date(matches[1]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Warning: Could not extract expiry date from certificate for ${domain}`);
|
||||
}
|
||||
|
||||
// Check if domain is already registered
|
||||
let domainInfo = this.domainCertificates.get(domain);
|
||||
if (!domainInfo) {
|
||||
// Register domain if not already registered
|
||||
domainInfo = {
|
||||
options: {
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
},
|
||||
certObtained: false,
|
||||
obtainingInProgress: false
|
||||
};
|
||||
this.domainCertificates.set(domain, domainInfo);
|
||||
}
|
||||
|
||||
// Set certificate
|
||||
domainInfo.certificate = certificate;
|
||||
domainInfo.privateKey = privateKey;
|
||||
domainInfo.certObtained = true;
|
||||
domainInfo.expiryDate = expiryDate;
|
||||
|
||||
console.log(`Loaded certificate for ${domain} from store, valid until ${expiryDate?.toISOString() || 'unknown'}`);
|
||||
} catch (err) {
|
||||
console.error(`Error loading certificate for ${domain}:`, err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading certificates from store:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain is a glob pattern
|
||||
@ -579,38 +456,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy initialization of the ACME client
|
||||
* @returns An ACME client instance
|
||||
*/
|
||||
private async getAcmeClient(): Promise<plugins.acme.Client> {
|
||||
if (this.acmeClient) {
|
||||
return this.acmeClient;
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate a new account key
|
||||
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
|
||||
|
||||
this.acmeClient = new plugins.acme.Client({
|
||||
directoryUrl: this.options.useProduction
|
||||
? plugins.acme.directory.letsencrypt.production
|
||||
: plugins.acme.directory.letsencrypt.staging,
|
||||
accountKey: this.accountKey,
|
||||
});
|
||||
|
||||
// Create a new account
|
||||
await this.acmeClient.createAccount({
|
||||
termsOfServiceAgreed: true,
|
||||
contact: [`mailto:${this.options.contactEmail}`],
|
||||
});
|
||||
|
||||
return this.acmeClient;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client';
|
||||
throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming HTTP requests
|
||||
@ -640,19 +485,32 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
const { domainInfo, pattern } = domainMatch;
|
||||
const options = domainInfo.options;
|
||||
|
||||
// If the request is for an ACME HTTP-01 challenge, handle it
|
||||
if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && (options.acmeMaintenance || options.acmeForward)) {
|
||||
// Check if we should forward ACME requests
|
||||
// Handle ACME HTTP-01 challenge requests or forwarding
|
||||
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
|
||||
// Forward ACME requests if configured
|
||||
if (options.acmeForward) {
|
||||
this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle ACME challenges for non-glob patterns
|
||||
if (!this.isGlobPattern(pattern)) {
|
||||
this.handleAcmeChallenge(req, res, domain);
|
||||
// If not managing ACME for this domain, return 404
|
||||
if (!options.acmeMaintenance) {
|
||||
res.statusCode = 404;
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
// Serve challenge response from in-memory storage
|
||||
const token = req.url.split('/').pop() || '';
|
||||
const keyAuth = this.acmeHttp01Storage.get(token);
|
||||
if (keyAuth) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(keyAuth);
|
||||
console.log(`Served ACME challenge response for ${domain}`);
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end('Challenge token not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we should forward non-ACME requests
|
||||
@ -762,292 +620,71 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves the ACME HTTP-01 challenge response
|
||||
* @param req The HTTP request
|
||||
* @param res The HTTP response
|
||||
* @param domain The domain for the challenge
|
||||
*/
|
||||
private handleAcmeChallenge(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, domain: string): void {
|
||||
const domainInfo = this.domainCertificates.get(domain);
|
||||
if (!domainInfo) {
|
||||
res.statusCode = 404;
|
||||
res.end('Domain not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
// The token is the last part of the URL
|
||||
const urlParts = req.url?.split('/');
|
||||
const token = urlParts ? urlParts[urlParts.length - 1] : '';
|
||||
|
||||
if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(domainInfo.challengeKeyAuthorization);
|
||||
console.log(`Served ACME challenge response for ${domain}`);
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end('Challenge token not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a certificate for a domain using ACME HTTP-01 challenge
|
||||
* @param domain The domain to obtain a certificate for
|
||||
* @param isRenewal Whether this is a renewal attempt
|
||||
*/
|
||||
/**
|
||||
* Obtains a certificate for a domain using SmartAcme HTTP-01 challenges
|
||||
* @param domain The domain to obtain a certificate for
|
||||
* @param isRenewal Whether this is a renewal attempt
|
||||
*/
|
||||
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
|
||||
// Don't allow certificate issuance for glob patterns
|
||||
if (this.isGlobPattern(domain)) {
|
||||
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
|
||||
}
|
||||
|
||||
// Get the domain info
|
||||
const domainInfo = this.domainCertificates.get(domain);
|
||||
if (!domainInfo) {
|
||||
throw new CertificateError('Domain not found', domain, isRenewal);
|
||||
}
|
||||
|
||||
// Verify that acmeMaintenance is enabled
|
||||
const domainInfo = this.domainCertificates.get(domain)!;
|
||||
if (!domainInfo.options.acmeMaintenance) {
|
||||
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent concurrent certificate issuance
|
||||
if (domainInfo.obtainingInProgress) {
|
||||
console.log(`Certificate issuance already in progress for ${domain}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.smartAcme) {
|
||||
throw new Port80HandlerError('SmartAcme is not initialized');
|
||||
}
|
||||
domainInfo.obtainingInProgress = true;
|
||||
domainInfo.lastRenewalAttempt = new Date();
|
||||
|
||||
try {
|
||||
const client = await this.getAcmeClient();
|
||||
|
||||
// Create a new order for the domain
|
||||
const order = await client.createOrder({
|
||||
identifiers: [{ type: 'dns', value: domain }],
|
||||
});
|
||||
|
||||
// Get the authorizations for the order
|
||||
const authorizations = await client.getAuthorizations(order);
|
||||
|
||||
// Process each authorization
|
||||
await this.processAuthorizations(client, domain, authorizations);
|
||||
|
||||
// Generate a CSR and private key
|
||||
const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
|
||||
commonName: domain,
|
||||
});
|
||||
|
||||
const csr = csrBuffer.toString();
|
||||
const privateKey = privateKeyBuffer.toString();
|
||||
|
||||
// Finalize the order with our CSR
|
||||
await client.finalizeOrder(order, csr);
|
||||
|
||||
// Get the certificate with the full chain
|
||||
const certificate = await client.getCertificate(order);
|
||||
|
||||
// Store the certificate and key
|
||||
// Request certificate via SmartAcme
|
||||
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
||||
const certificate = certObj.publicKey;
|
||||
const privateKey = certObj.privateKey;
|
||||
const expiryDate = new Date(certObj.validUntil);
|
||||
domainInfo.certificate = certificate;
|
||||
domainInfo.privateKey = privateKey;
|
||||
domainInfo.certObtained = true;
|
||||
|
||||
// Clear challenge data
|
||||
delete domainInfo.challengeToken;
|
||||
delete domainInfo.challengeKeyAuthorization;
|
||||
|
||||
// Extract expiry date from certificate
|
||||
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
||||
domainInfo.expiryDate = expiryDate;
|
||||
|
||||
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
|
||||
|
||||
// Save the certificate to the store if enabled
|
||||
if (this.options.certificateStore) {
|
||||
this.saveCertificateToStore(domain, certificate, privateKey);
|
||||
}
|
||||
|
||||
// Emit the appropriate event
|
||||
const eventType = isRenewal
|
||||
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
||||
// Persistence moved to CertProvisioner
|
||||
const eventType = isRenewal
|
||||
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
||||
: Port80HandlerEvents.CERTIFICATE_ISSUED;
|
||||
|
||||
this.emitCertificateEvent(eventType, {
|
||||
domain,
|
||||
certificate,
|
||||
privateKey,
|
||||
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
||||
expiryDate: expiryDate || this.getDefaultExpiryDate()
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
// Check for rate limit errors
|
||||
if (error.message && (
|
||||
error.message.includes('rateLimited') ||
|
||||
error.message.includes('too many certificates') ||
|
||||
error.message.includes('rate limit')
|
||||
)) {
|
||||
console.error(`Rate limit reached for ${domain}. Waiting before retry.`);
|
||||
} else {
|
||||
console.error(`Error during certificate issuance for ${domain}:`, error);
|
||||
}
|
||||
|
||||
// Emit failure event
|
||||
const errorMsg = error?.message || 'Unknown error';
|
||||
console.error(`Error during certificate issuance for ${domain}:`, error);
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
||||
domain,
|
||||
error: error.message || 'Unknown error',
|
||||
error: errorMsg,
|
||||
isRenewal
|
||||
} as ICertificateFailure);
|
||||
|
||||
throw new CertificateError(
|
||||
error.message || 'Certificate issuance failed',
|
||||
domain,
|
||||
isRenewal
|
||||
);
|
||||
throw new CertificateError(errorMsg, domain, isRenewal);
|
||||
} finally {
|
||||
// Reset flag whether successful or not
|
||||
domainInfo.obtainingInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process ACME authorizations by verifying and completing challenges
|
||||
* @param client ACME client
|
||||
* @param domain Domain name
|
||||
* @param authorizations Authorizations to process
|
||||
*/
|
||||
private async processAuthorizations(
|
||||
client: plugins.acme.Client,
|
||||
domain: string,
|
||||
authorizations: plugins.acme.Authorization[]
|
||||
): Promise<void> {
|
||||
const domainInfo = this.domainCertificates.get(domain);
|
||||
if (!domainInfo) {
|
||||
throw new CertificateError('Domain not found during authorization', domain);
|
||||
}
|
||||
|
||||
for (const authz of authorizations) {
|
||||
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
||||
if (!challenge) {
|
||||
throw new CertificateError('HTTP-01 challenge not found', domain);
|
||||
}
|
||||
|
||||
// Get the key authorization for the challenge
|
||||
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
||||
|
||||
// Store the challenge data
|
||||
domainInfo.challengeToken = challenge.token;
|
||||
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
||||
|
||||
// ACME client type definition workaround - use compatible approach
|
||||
// First check if challenge verification is needed
|
||||
const authzUrl = authz.url;
|
||||
|
||||
try {
|
||||
// Check if authzUrl exists and perform verification
|
||||
if (authzUrl) {
|
||||
await client.verifyChallenge(authz, challenge);
|
||||
}
|
||||
|
||||
// Complete the challenge
|
||||
await client.completeChallenge(challenge);
|
||||
|
||||
// Wait for validation
|
||||
await client.waitForValidStatus(challenge);
|
||||
console.log(`HTTP-01 challenge completed for ${domain}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error';
|
||||
console.error(`Challenge error for ${domain}:`, error);
|
||||
throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the certificate renewal timer
|
||||
*/
|
||||
private startRenewalTimer(): void {
|
||||
if (this.renewalTimer) {
|
||||
clearInterval(this.renewalTimer);
|
||||
}
|
||||
|
||||
// Convert hours to milliseconds
|
||||
const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
|
||||
|
||||
this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
|
||||
|
||||
// Prevent the timer from keeping the process alive
|
||||
if (this.renewalTimer.unref) {
|
||||
this.renewalTimer.unref();
|
||||
}
|
||||
|
||||
console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for certificates that need renewal
|
||||
*/
|
||||
private checkForRenewals(): void {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip renewal if auto-renewal is disabled
|
||||
if (this.options.autoRenew === false) {
|
||||
console.log('Auto-renewal is disabled, skipping certificate renewal check');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Checking for certificates that need renewal...');
|
||||
|
||||
const now = new Date();
|
||||
const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
||||
// Skip glob patterns
|
||||
if (this.isGlobPattern(domain)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip domains with acmeMaintenance disabled
|
||||
if (!domainInfo.options.acmeMaintenance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip domains without certificates or already in renewal
|
||||
if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip domains without expiry dates
|
||||
if (!domainInfo.expiryDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
|
||||
|
||||
// Check if certificate is near expiry
|
||||
if (timeUntilExpiry <= renewThresholdMs) {
|
||||
console.log(`Certificate for ${domain} expires soon, renewing...`);
|
||||
|
||||
const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
|
||||
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
|
||||
domain,
|
||||
expiryDate: domainInfo.expiryDate,
|
||||
daysRemaining
|
||||
} as ICertificateExpiring);
|
||||
|
||||
// Start renewal process
|
||||
this.obtainCertificate(domain, true).catch(err => {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`Error renewing certificate for ${domain}:`, errorMessage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract expiry date from certificate using a more robust approach
|
||||
@ -1176,4 +813,16 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
public getConfig(): Required<IPort80HandlerOptions> {
|
||||
return { ...this.options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a certificate renewal for a specific domain.
|
||||
* @param domain The domain to renew.
|
||||
*/
|
||||
public async renewCertificate(domain: string): Promise<void> {
|
||||
if (!this.domainCertificates.has(domain)) {
|
||||
throw new Port80HandlerError(`Domain not managed: ${domain}`);
|
||||
}
|
||||
// Trigger renewal via ACME
|
||||
await this.obtainCertificate(domain, true);
|
||||
}
|
||||
}
|
183
ts/smartproxy/classes.pp.certprovisioner.ts
Normal file
183
ts/smartproxy/classes.pp.certprovisioner.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js';
|
||||
import { Port80Handler, Port80HandlerEvents, type ICertificateData } from '../port80handler/classes.port80handler.js';
|
||||
import type { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
||||
|
||||
/**
|
||||
* CertProvisioner manages certificate provisioning and renewal workflows,
|
||||
* unifying static certificates and HTTP-01 challenges via Port80Handler.
|
||||
*/
|
||||
export class CertProvisioner extends plugins.EventEmitter {
|
||||
private domainConfigs: IDomainConfig[];
|
||||
private port80Handler: Port80Handler;
|
||||
private networkProxyBridge: NetworkProxyBridge;
|
||||
private certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>;
|
||||
private renewThresholdDays: number;
|
||||
private renewCheckIntervalHours: number;
|
||||
private autoRenew: boolean;
|
||||
private renewManager?: plugins.taskbuffer.TaskManager;
|
||||
// Track provisioning type per domain: 'http01' or 'static'
|
||||
private provisionMap: Map<string, 'http01' | 'static'>;
|
||||
|
||||
/**
|
||||
* @param domainConfigs Array of domain configuration objects
|
||||
* @param port80Handler HTTP-01 challenge handler instance
|
||||
* @param networkProxyBridge Bridge for applying external certificates
|
||||
* @param certProvider Optional callback returning a static cert or 'http01'
|
||||
* @param renewThresholdDays Days before expiry to trigger renewals
|
||||
* @param renewCheckIntervalHours Interval in hours to check for renewals
|
||||
* @param autoRenew Whether to automatically schedule renewals
|
||||
*/
|
||||
constructor(
|
||||
domainConfigs: IDomainConfig[],
|
||||
port80Handler: Port80Handler,
|
||||
networkProxyBridge: NetworkProxyBridge,
|
||||
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>,
|
||||
renewThresholdDays: number = 30,
|
||||
renewCheckIntervalHours: number = 24,
|
||||
autoRenew: boolean = true,
|
||||
forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }> = []
|
||||
) {
|
||||
super();
|
||||
this.domainConfigs = domainConfigs;
|
||||
this.port80Handler = port80Handler;
|
||||
this.networkProxyBridge = networkProxyBridge;
|
||||
this.certProvider = certProvider;
|
||||
this.renewThresholdDays = renewThresholdDays;
|
||||
this.renewCheckIntervalHours = renewCheckIntervalHours;
|
||||
this.autoRenew = autoRenew;
|
||||
this.provisionMap = new Map();
|
||||
this.forwardConfigs = forwardConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start initial provisioning and schedule renewals.
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Subscribe to Port80Handler certificate events
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
|
||||
this.emit('certificate', { ...data, source: 'http01', isRenewal: false });
|
||||
});
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
|
||||
this.emit('certificate', { ...data, source: 'http01', isRenewal: true });
|
||||
});
|
||||
|
||||
// Apply external forwarding for ACME challenges (e.g. Synology)
|
||||
for (const f of this.forwardConfigs) {
|
||||
this.port80Handler.addDomain({
|
||||
domainName: f.domain,
|
||||
sslRedirect: f.sslRedirect,
|
||||
acmeMaintenance: false,
|
||||
forward: f.forwardConfig,
|
||||
acmeForward: f.acmeForwardConfig
|
||||
});
|
||||
}
|
||||
// Initial provisioning for all domains
|
||||
const domains = this.domainConfigs.flatMap(cfg => cfg.domains);
|
||||
for (const domain of domains) {
|
||||
// Skip wildcard domains
|
||||
if (domain.includes('*')) continue;
|
||||
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
||||
if (this.certProvider) {
|
||||
try {
|
||||
provision = await this.certProvider(domain);
|
||||
} catch (err) {
|
||||
console.error(`certProvider error for ${domain}:`, err);
|
||||
}
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
this.provisionMap.set(domain, 'http01');
|
||||
this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true });
|
||||
} else {
|
||||
this.provisionMap.set(domain, 'static');
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
certificate: certObj.publicKey,
|
||||
privateKey: certObj.privateKey,
|
||||
expiryDate: new Date(certObj.validUntil)
|
||||
};
|
||||
this.networkProxyBridge.applyExternalCertificate(certData);
|
||||
this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule renewals if enabled
|
||||
if (this.autoRenew) {
|
||||
this.renewManager = new plugins.taskbuffer.TaskManager();
|
||||
const renewTask = new plugins.taskbuffer.Task({
|
||||
name: 'CertificateRenewals',
|
||||
taskFunction: async () => {
|
||||
for (const [domain, type] of this.provisionMap.entries()) {
|
||||
// Skip wildcard domains
|
||||
if (domain.includes('*')) continue;
|
||||
try {
|
||||
if (type === 'http01') {
|
||||
await this.port80Handler.renewCertificate(domain);
|
||||
} else if (type === 'static' && this.certProvider) {
|
||||
const provision2 = await this.certProvider(domain);
|
||||
if (provision2 !== 'http01') {
|
||||
const certObj = provision2 as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
certificate: certObj.publicKey,
|
||||
privateKey: certObj.privateKey,
|
||||
expiryDate: new Date(certObj.validUntil)
|
||||
};
|
||||
this.networkProxyBridge.applyExternalCertificate(certData);
|
||||
this.emit('certificate', { ...certData, source: 'static', isRenewal: true });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Renewal error for ${domain}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const hours = this.renewCheckIntervalHours;
|
||||
const cronExpr = `0 0 */${hours} * * *`;
|
||||
this.renewManager.addAndScheduleTask(renewTask, cronExpr);
|
||||
this.renewManager.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all scheduled renewal tasks.
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
// Stop scheduled renewals
|
||||
if (this.renewManager) {
|
||||
this.renewManager.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a certificate on-demand for the given domain.
|
||||
* @param domain Domain name to provision
|
||||
*/
|
||||
public async requestCertificate(domain: string): Promise<void> {
|
||||
// Skip wildcard domains
|
||||
if (domain.includes('*')) {
|
||||
throw new Error(`Cannot request certificate for wildcard domain: ${domain}`);
|
||||
}
|
||||
// Determine provisioning method
|
||||
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
||||
if (this.certProvider) {
|
||||
provision = await this.certProvider(domain);
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
await this.port80Handler.renewCertificate(domain);
|
||||
} else {
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
certificate: certObj.publicKey,
|
||||
privateKey: certObj.privateKey,
|
||||
expiryDate: new Date(certObj.validUntil)
|
||||
};
|
||||
this.networkProxyBridge.applyExternalCertificate(certData);
|
||||
this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Provision object for static or HTTP-01 certificate
|
||||
*/
|
||||
export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
|
||||
|
||||
/** Domain configuration with per-domain allowed port ranges */
|
||||
export interface IDomainConfig {
|
||||
domains: string[]; // Glob patterns for domain(s)
|
||||
@ -104,17 +109,11 @@ export interface IPortProxySettings {
|
||||
}>;
|
||||
};
|
||||
|
||||
// Legacy ACME configuration (deprecated, use port80HandlerConfig instead)
|
||||
acme?: {
|
||||
enabled?: boolean;
|
||||
port?: number;
|
||||
contactEmail?: string;
|
||||
useProduction?: boolean;
|
||||
renewThresholdDays?: number;
|
||||
autoRenew?: boolean;
|
||||
certificateStore?: string;
|
||||
skipConfiguredCerts?: boolean;
|
||||
};
|
||||
/**
|
||||
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
||||
* or a static certificate object for immediate provisioning.
|
||||
*/
|
||||
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,10 +43,6 @@ export class NetworkProxyBridge {
|
||||
useExternalPort80Handler: !!this.port80Handler // Use Port80Handler if available
|
||||
};
|
||||
|
||||
// Copy ACME settings for backward compatibility (if port80HandlerConfig not set)
|
||||
if (!this.settings.port80HandlerConfig && this.settings.acme) {
|
||||
networkProxyOptions.acme = { ...this.settings.acme };
|
||||
}
|
||||
|
||||
this.networkProxy = new NetworkProxy(networkProxyOptions);
|
||||
|
||||
@ -95,6 +91,17 @@ export class NetworkProxyBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an external (static) certificate into NetworkProxy
|
||||
*/
|
||||
public applyExternalCertificate(data: ICertificateData): void {
|
||||
if (!this.networkProxy) {
|
||||
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
|
||||
return;
|
||||
}
|
||||
this.handleCertificateEvent(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the NetworkProxy instance
|
||||
*/
|
||||
@ -277,7 +284,7 @@ export class NetworkProxyBridge {
|
||||
);
|
||||
|
||||
// Log ACME-eligible domains
|
||||
const acmeEnabled = this.settings.port80HandlerConfig?.enabled || this.settings.acme?.enabled;
|
||||
const acmeEnabled = !!this.settings.port80HandlerConfig?.enabled;
|
||||
if (acmeEnabled) {
|
||||
const acmeEligibleDomains = proxyConfigs
|
||||
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
|
||||
@ -338,7 +345,7 @@ export class NetworkProxyBridge {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.settings.port80HandlerConfig?.enabled && !this.settings.acme?.enabled) {
|
||||
if (!this.settings.port80HandlerConfig?.enabled) {
|
||||
console.log('Cannot request certificate - ACME is not enabled');
|
||||
return false;
|
||||
}
|
||||
|
@ -117,10 +117,6 @@ export class PortRangeManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Add ACME HTTP challenge port if enabled
|
||||
if (this.settings.acme?.enabled && this.settings.acme.port) {
|
||||
ports.add(this.settings.acme.port);
|
||||
}
|
||||
|
||||
// Add global port ranges
|
||||
if (this.settings.globalPortRanges) {
|
||||
@ -202,12 +198,6 @@ export class PortRangeManager {
|
||||
warnings.push(`NetworkProxy port ${this.settings.networkProxyPort} is also used in port ranges`);
|
||||
}
|
||||
|
||||
// Check ACME port
|
||||
if (this.settings.acme?.enabled && this.settings.acme.port) {
|
||||
if (portMappings.has(this.settings.acme.port)) {
|
||||
warnings.push(`ACME HTTP challenge port ${this.settings.acme.port} is also used in port ranges`);
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
@ -8,14 +8,16 @@ import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
||||
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
||||
import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
||||
import { ConnectionHandler } from './classes.pp.connectionhandler.js';
|
||||
import { Port80Handler, Port80HandlerEvents } from '../port80handler/classes.port80handler.js';
|
||||
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
||||
import { CertProvisioner } from './classes.pp.certprovisioner.js';
|
||||
import type { ICertificateData } from '../port80handler/classes.port80handler.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* SmartProxy - Main class that coordinates all components
|
||||
*/
|
||||
export class SmartProxy {
|
||||
export class SmartProxy extends plugins.EventEmitter {
|
||||
private netServers: plugins.net.Server[] = [];
|
||||
private connectionLogger: NodeJS.Timeout | null = null;
|
||||
private isShuttingDown: boolean = false;
|
||||
@ -32,8 +34,11 @@ export class SmartProxy {
|
||||
|
||||
// Port80Handler for ACME certificate management
|
||||
private port80Handler: Port80Handler | null = null;
|
||||
// CertProvisioner for unified certificate workflows
|
||||
private certProvisioner?: CertProvisioner;
|
||||
|
||||
constructor(settingsArg: IPortProxySettings) {
|
||||
super();
|
||||
// Set reasonable defaults for all settings
|
||||
this.settings = {
|
||||
...settingsArg,
|
||||
@ -66,37 +71,20 @@ export class SmartProxy {
|
||||
globalPortRanges: settingsArg.globalPortRanges || [],
|
||||
};
|
||||
|
||||
// Set port80HandlerConfig defaults, using legacy acme config if available
|
||||
// Set default port80HandlerConfig if not provided
|
||||
if (!this.settings.port80HandlerConfig || Object.keys(this.settings.port80HandlerConfig).length === 0) {
|
||||
if (this.settings.acme) {
|
||||
// Migrate from legacy acme config
|
||||
this.settings.port80HandlerConfig = {
|
||||
enabled: this.settings.acme.enabled,
|
||||
port: this.settings.acme.port || 80,
|
||||
contactEmail: this.settings.acme.contactEmail || 'admin@example.com',
|
||||
useProduction: this.settings.acme.useProduction || false,
|
||||
renewThresholdDays: this.settings.acme.renewThresholdDays || 30,
|
||||
autoRenew: this.settings.acme.autoRenew !== false, // Default to true
|
||||
certificateStore: this.settings.acme.certificateStore || './certs',
|
||||
skipConfiguredCerts: this.settings.acme.skipConfiguredCerts || false,
|
||||
httpsRedirectPort: this.settings.fromPort,
|
||||
renewCheckIntervalHours: 24
|
||||
};
|
||||
} else {
|
||||
// Set defaults if no config provided
|
||||
this.settings.port80HandlerConfig = {
|
||||
enabled: false,
|
||||
port: 80,
|
||||
contactEmail: 'admin@example.com',
|
||||
useProduction: false,
|
||||
renewThresholdDays: 30,
|
||||
autoRenew: true,
|
||||
certificateStore: './certs',
|
||||
skipConfiguredCerts: false,
|
||||
httpsRedirectPort: this.settings.fromPort,
|
||||
renewCheckIntervalHours: 24
|
||||
};
|
||||
}
|
||||
this.settings.port80HandlerConfig = {
|
||||
enabled: false,
|
||||
port: 80,
|
||||
contactEmail: 'admin@example.com',
|
||||
useProduction: false,
|
||||
renewThresholdDays: 30,
|
||||
autoRenew: true,
|
||||
certificateStore: './certs',
|
||||
skipConfiguredCerts: false,
|
||||
httpsRedirectPort: this.settings.fromPort,
|
||||
renewCheckIntervalHours: 24
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize component managers
|
||||
@ -156,62 +144,13 @@ export class SmartProxy {
|
||||
port: config.port,
|
||||
contactEmail: config.contactEmail,
|
||||
useProduction: config.useProduction,
|
||||
renewThresholdDays: config.renewThresholdDays,
|
||||
httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort,
|
||||
renewCheckIntervalHours: config.renewCheckIntervalHours,
|
||||
enabled: config.enabled,
|
||||
autoRenew: config.autoRenew,
|
||||
certificateStore: config.certificateStore,
|
||||
skipConfiguredCerts: config.skipConfiguredCerts
|
||||
});
|
||||
|
||||
// Register domain forwarding configurations
|
||||
if (config.domainForwards) {
|
||||
for (const forward of config.domainForwards) {
|
||||
this.port80Handler.addDomain({
|
||||
domainName: forward.domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true,
|
||||
forward: forward.forwardConfig,
|
||||
acmeForward: forward.acmeForwardConfig
|
||||
});
|
||||
|
||||
console.log(`Registered domain forwarding for ${forward.domain}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Register all non-wildcard domains from domain configs
|
||||
for (const domainConfig of this.settings.domainConfigs) {
|
||||
for (const domain of domainConfig.domains) {
|
||||
// Skip wildcards
|
||||
if (domain.includes('*')) continue;
|
||||
|
||||
this.port80Handler.addDomain({
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
});
|
||||
|
||||
console.log(`Registered domain ${domain} with Port80Handler`);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (certData) => {
|
||||
console.log(`Certificate issued for ${certData.domain}, valid until ${certData.expiryDate.toISOString()}`);
|
||||
});
|
||||
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (certData) => {
|
||||
console.log(`Certificate renewed for ${certData.domain}, valid until ${certData.expiryDate.toISOString()}`);
|
||||
});
|
||||
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (failureData) => {
|
||||
console.log(`Certificate ${failureData.isRenewal ? 'renewal' : 'issuance'} failed for ${failureData.domain}: ${failureData.error}`);
|
||||
});
|
||||
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (expiryData) => {
|
||||
console.log(`Certificate for ${expiryData.domain} is expiring in ${expiryData.daysRemaining} days`);
|
||||
});
|
||||
|
||||
// Share Port80Handler with NetworkProxyBridge
|
||||
this.networkProxyBridge.setPort80Handler(this.port80Handler);
|
||||
@ -236,6 +175,37 @@ export class SmartProxy {
|
||||
|
||||
// Initialize Port80Handler if enabled
|
||||
await this.initializePort80Handler();
|
||||
// Initialize CertProvisioner for unified certificate workflows
|
||||
if (this.port80Handler) {
|
||||
this.certProvisioner = new CertProvisioner(
|
||||
this.settings.domainConfigs,
|
||||
this.port80Handler,
|
||||
this.networkProxyBridge,
|
||||
this.settings.certProvider,
|
||||
this.settings.port80HandlerConfig?.renewThresholdDays || 30,
|
||||
this.settings.port80HandlerConfig?.renewCheckIntervalHours || 24,
|
||||
this.settings.port80HandlerConfig?.autoRenew !== false,
|
||||
// External ACME forwarding for specific domains
|
||||
this.settings.port80HandlerConfig?.domainForwards?.map(f => ({
|
||||
domain: f.domain,
|
||||
forwardConfig: f.forwardConfig,
|
||||
acmeForwardConfig: f.acmeForwardConfig,
|
||||
sslRedirect: false
|
||||
})) || []
|
||||
);
|
||||
this.certProvisioner.on('certificate', (certData) => {
|
||||
this.emit('certificate', {
|
||||
domain: certData.domain,
|
||||
publicKey: certData.certificate,
|
||||
privateKey: certData.privateKey,
|
||||
expiryDate: certData.expiryDate,
|
||||
source: certData.source,
|
||||
isRenewal: certData.isRenewal
|
||||
});
|
||||
});
|
||||
await this.certProvisioner.start();
|
||||
console.log('CertProvisioner started');
|
||||
}
|
||||
|
||||
// Initialize and start NetworkProxy if needed
|
||||
if (
|
||||
@ -364,6 +334,11 @@ export class SmartProxy {
|
||||
public async stop() {
|
||||
console.log('PortProxy shutting down...');
|
||||
this.isShuttingDown = true;
|
||||
// Stop CertProvisioner if active
|
||||
if (this.certProvisioner) {
|
||||
await this.certProvisioner.stop();
|
||||
console.log('CertProvisioner stopped');
|
||||
}
|
||||
|
||||
// Stop the Port80Handler if running
|
||||
if (this.port80Handler) {
|
||||
@ -429,22 +404,40 @@ export class SmartProxy {
|
||||
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
|
||||
}
|
||||
|
||||
// If Port80Handler is running, register non-wildcard domains
|
||||
// If Port80Handler is running, provision certificates per new domain
|
||||
if (this.port80Handler && this.settings.port80HandlerConfig?.enabled) {
|
||||
for (const domainConfig of newDomainConfigs) {
|
||||
for (const domain of domainConfig.domains) {
|
||||
// Skip wildcards
|
||||
if (domain.includes('*')) continue;
|
||||
|
||||
this.port80Handler.addDomain({
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
});
|
||||
let provision = 'http01' as string | plugins.tsclass.network.ICert;
|
||||
if (this.settings.certProvider) {
|
||||
try {
|
||||
provision = await this.settings.certProvider(domain);
|
||||
} catch (err) {
|
||||
console.log(`certProvider error for ${domain}: ${err}`);
|
||||
}
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
this.port80Handler.addDomain({
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
});
|
||||
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
||||
} else {
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
certificate: certObj.publicKey,
|
||||
privateKey: certObj.privateKey,
|
||||
expiryDate: new Date(certObj.validUntil)
|
||||
};
|
||||
this.networkProxyBridge.applyExternalCertificate(certData);
|
||||
console.log(`Applied static certificate for ${domain} from certProvider`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Registered non-wildcard domains with Port80Handler');
|
||||
console.log('Provisioned certificates for new domains');
|
||||
}
|
||||
}
|
||||
|
||||
@ -515,6 +508,27 @@ export class SmartProxy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform scheduled renewals for managed domains
|
||||
*/
|
||||
private async performRenewals(): Promise<void> {
|
||||
if (!this.port80Handler) return;
|
||||
const statuses = this.port80Handler.getDomainCertificateStatus();
|
||||
const threshold = this.settings.port80HandlerConfig.renewThresholdDays ?? 30;
|
||||
const now = new Date();
|
||||
for (const [domain, status] of statuses.entries()) {
|
||||
if (!status.certObtained || status.obtainingInProgress || !status.expiryDate) continue;
|
||||
const msRemaining = status.expiryDate.getTime() - now.getTime();
|
||||
const daysRemaining = Math.ceil(msRemaining / (24 * 60 * 60 * 1000));
|
||||
if (daysRemaining <= threshold) {
|
||||
try {
|
||||
await this.port80Handler.renewCertificate(domain);
|
||||
} catch (err) {
|
||||
console.error(`Error renewing certificate for ${domain}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Request a certificate for a specific domain
|
||||
*/
|
||||
|
Reference in New Issue
Block a user