initial commit

This commit is contained in:
2025-11-14 20:38:30 +01:00
parent ff91337962
commit ce1710dbd2
22 changed files with 1055 additions and 1 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*
*.client.json
src/local.config.ts

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
*/**/local.*
# The client configs for this application
*.client.json
docker-compose.local.yml

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# [optional] tests & build
ENV NODE_ENV=production
#RUN bun test
RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/dist/index.mjs .
COPY --from=prerelease /usr/src/app/package.json .
# run the app
USER bun
#EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "index.mjs" ]

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2025 Enrico Bühler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,3 +1,66 @@
# nanokvm-mqtt
Exposes NanoKVM API via MQTT for use with home assistant
Exposes NanoKVM API via MQTT with support for home assistant autodiscovery/config.
## Implementation
This project is built with TypeScript and enforces runtime type safety where possible by using [zod](https://zod.dev).
## Disclaimer
Please read the License and understand that this program comes with **no warranties or guarantees**.
**Critical Security Warning:** Exposing a KVM (Keyboard, Video, Mouse) device over a network creates significant security risks, as it provides direct access to connected systems. Carefully review our security advisories below before deployment.
## Usage
1. Clone the repository
2. Create one or multiple client configs like ```xxx.client.json```
3. **Option A (recommended)** Use docker-compose like in the provided example ```docker-compose.example.yml```
3. **Option B** Install bun and run via ```bun run build && bun run start```
4. Your NanoKVM(s) should now show up in home assistant via autodiscovery
## Security Notices
### NanoKVM
**Critical vulnerability:** The NanoKVM firmware currently uses a hardcoded secret key for authentication.
Thats only one of the many security flaws.
By default, this project includes the known hardcoded secret for compatibility.
We strongly recommend blocking every connection going in and out of the NanoKVM by default,
and only allow as narrow access as possible to the web server (port 80) with return traffic.
Its also recommended to only enable SSH when you need it.
### Additional Security Recommendations
- **TLS/SSL:** Use encrypted MQTT connections (mqtts://) with valid certificates
- **MQTT Authentication:** Enable username/password authentication on your MQTT broker
- **Access Control:** Implement MQTT ACLs (Access Control Lists) to restrict topic access
- **Firewall Rules:** Block external access; only allow connections from trusted IPs
- **Regular Updates:** Monitor for NanoKVM firmware and dependency updates
### Usage with Home Assistant
**Think carefully before integrating this with Home Assistant**, especially if:
- Your Home Assistant instance is publicly accessible
- You use cloud-based integrations or remote access features
- Multiple users have access to your Home Assistant dashboard
**Recommended mitigations:**
- Keep Home Assistant on a private network only
- Use VPN access instead of port forwarding
- Monitor access logs regularly
- Consider if you truly need KVM control through Home Assistant
### Known Risks
Potential attack vectors include:
- Unauthorized access to connected computers/servers
- Keystroke injection and command execution
- Screen capture and information disclosure
- BIOS/firmware manipulation on connected systems
- Lateral movement within your network

247
bun.lock Normal file
View File

@@ -0,0 +1,247 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "nanokvm-mqtt",
"dependencies": {
"mqtt": "^5.14.1",
"tsdown": "^0.16.4",
"tslog": "^4.10.2",
"zod": "^4.1.12",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@emnapi/core": ["@emnapi/core@1.7.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.7.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
"@oxc-project/runtime": ["@oxc-project/runtime@0.97.0", "", {}, "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w=="],
"@oxc-project/types": ["@oxc-project/types@0.97.0", "", {}, "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ=="],
"@quansync/fs": ["@quansync/fs@0.1.5", "", { "dependencies": { "quansync": "^0.2.11" } }, "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "x64" }, "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.50", "", { "os": "freebsd", "cpu": "x64" }, "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm" }, "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.50", "", { "os": "none", "cpu": "arm64" }, "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.50", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "arm64" }, "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA=="],
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "ia32" }, "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "x64" }, "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
"@types/readable-stream": ["@types/readable-stream@4.0.22", "", { "dependencies": { "@types/node": "*" } }, "sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
"ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"birpc": ["birpc@2.8.0", "", {}, "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw=="],
"bl": ["bl@6.1.4", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-ZV/9asSuknOExbM/zPPA8z00lc1ihPKWaStHkkQrxHNeYx+yY+TmF+v80dpv2G0mv3HVXBu7ryoAsxbFFhf4eg=="],
"broker-factory": ["broker-factory@3.1.10", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1", "worker-factory": "^7.0.46" } }, "sha512-BzqK5GYFhvVFvO13uzPN0SCiOsOQuhMUbsGvTXDJMA2/N4GvIlFdxEuueE+60Zk841bBU5G3+fl2cqYEo0wgGg=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"commist": ["commist@3.2.0", "", {}, "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw=="],
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="],
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"fast-unique-numbers": ["fast-unique-numbers@9.0.24", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1" } }, "sha512-Dv0BYn4waOWse94j16rsZ5w/0zoaCa74O3q6IZjMqaXbtT92Q+Sb6pPk+phGzD8Xh+nueQmSRI3tSCaHKidzKw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"js-sdsl": ["js-sdsl@4.3.0", "", {}, "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"mqtt": ["mqtt@5.14.1", "", { "dependencies": { "@types/readable-stream": "^4.0.21", "@types/ws": "^8.18.1", "commist": "^3.2.0", "concat-stream": "^2.0.0", "debug": "^4.4.1", "help-me": "^5.0.0", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "mqtt-packet": "^9.0.2", "number-allocator": "^1.0.14", "readable-stream": "^4.7.0", "rfdc": "^1.4.1", "socks": "^2.8.6", "split2": "^4.2.0", "worker-timers": "^8.0.23", "ws": "^8.18.3" }, "bin": { "mqtt_pub": "build/bin/pub.js", "mqtt_sub": "build/bin/sub.js", "mqtt": "build/bin/mqtt.js" } }, "sha512-NxkPxE70Uq3Ph7goefQa7ggSsVzHrayCD0OyxlJgITN/EbzlZN+JEPmaAZdxP1LsIT5FamDyILoQTF72W7Nnbw=="],
"mqtt-packet": ["mqtt-packet@9.0.2", "", { "dependencies": { "bl": "^6.0.8", "debug": "^4.3.4", "process-nextick-args": "^2.0.1" } }, "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"number-allocator": ["number-allocator@1.0.14", "", { "dependencies": { "debug": "^4.3.1", "js-sdsl": "4.3.0" } }, "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA=="],
"obug": ["obug@1.0.0", "", { "peerDependencies": { "ms": "^2.0.0" }, "optionalPeers": ["ms"] }, "sha512-WKcS43Yl6YPJekid7KiRdT6CHMSmYWVfJiSFbTaGxWQlC+cEBPxHa9jR1uS2cMiQmXd8Hsa2ipAKErQ/GLhSpg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="],
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.17.7", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ast-kit": "^2.2.0", "birpc": "^2.8.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.0", "magic-string": "^0.30.21", "obug": "^1.0.0" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.44", "typescript": "^5.0.0", "vue-tsc": "~3.1.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-ZGgXMhzCItmznNzbJlTcC/KdM6bIwcZoYUykJ2q14HOGvnMhnl2RXU+XrIrdjA2Hyzq3nWqDH7qWaM5a4uCVnw=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"tsdown": ["tsdown@0.16.4", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "obug": "^1.0.0", "rolldown": "1.0.0-beta.50", "rolldown-plugin-dts": "^0.17.7", "semver": "^7.7.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig-core": "^7.4.1", "unrun": "^0.2.9" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@vitejs/devtools": "^0.0.0-alpha.17", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "@vitejs/devtools", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-tdhy+EQpZSVrVkDRnjKEKVfh1git2HrliGp3SylRNg7kk+lOx3SvT7NLakKX5grPAg8WrZrXWLPUrCegWupqgg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tslog": ["tslog@4.10.2", "", {}, "sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g=="],
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"unconfig-core": ["unconfig-core@7.4.1", "", { "dependencies": { "@quansync/fs": "^0.1.5", "quansync": "^0.2.11" } }, "sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"unrun": ["unrun@0.2.9", "", { "dependencies": { "@oxc-project/runtime": "^0.97.0", "rolldown": "1.0.0-beta.50" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-Cta7uGK/08OqH2O0HLYXs1AfIBp2+2v11ZoeAIqJLUCb9CN+7uxj+CldHCzqAw30b8MJEmWe+BFgK2sl4lJXlw=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"worker-factory": ["worker-factory@7.0.46", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1" } }, "sha512-Sr1hq2FMgNa04UVhYQacsw+i58BtMimzDb4+CqYphZ97OfefRpURu0UZ+JxMr/H36VVJBfuVkxTK7MytsanC3w=="],
"worker-timers": ["worker-timers@8.0.25", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1", "worker-timers-broker": "^8.0.11", "worker-timers-worker": "^9.0.11" } }, "sha512-X7Z5dmM6PlrEnaadtFQOyXHGD/IysPA3HZzaC2koqsU1VI+RvyGmjiiLiUBQixK8PH5R7ilkOzZupWskNRaXmA=="],
"worker-timers-broker": ["worker-timers-broker@8.0.11", "", { "dependencies": { "@babel/runtime": "^7.28.4", "broker-factory": "^3.1.10", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1", "worker-timers-worker": "^9.0.11" } }, "sha512-uwhxKru8BI9m2tsogxr2fB6POZ8LB2xH+Pu3R0mvQnAZLPgLD6K3IX4LNKPTEgTJ/j5VsuQPB+gLI1NBNKkPlg=="],
"worker-timers-worker": ["worker-timers-worker@9.0.11", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1", "worker-factory": "^7.0.46" } }, "sha512-pArb5xtgHWImYpXhjg1OFv7JFG0ubmccb73TFoXHXjG830fFj+16N57q9YeBnZX52dn+itRrMoJZ9HaZBVzDaA=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
"concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
}
}

1
config-schema.json Normal file
View File

@@ -0,0 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"nanoKvm":{"type":"object","properties":{"host":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"},"passwordSecret":{"default":"nanokvm-sipeed-2024","type":"string"},"protocol":{"default":"http","type":"string","enum":["http","https"]},"token":{"type":"string"}},"required":["host","username","password","passwordSecret","protocol"],"additionalProperties":false},"mqtt":{"type":"object","properties":{"password":{"type":"string"},"user":{"type":"string"},"host":{"type":"string"},"port":{"type":"number"}},"required":["host","port"],"additionalProperties":false}},"required":["nanoKvm","mqtt"],"additionalProperties":false}

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "nanokvm-mqtt",
"module": "index.ts",
"type": "module",
"private": true,
"scripts": {
"dev": "bun --watch ./src/index.ts",
"build": "tsdown",
"start": "bun run ./dist/index.mjs"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"mqtt": "^5.14.1",
"tsdown": "^0.16.4",
"tslog": "^4.10.2",
"zod": "^4.1.12"
}
}

View File

@@ -0,0 +1,8 @@
import z from "zod";
export const MQTTClientOptions = z.object({
password: z.string().optional(),
user: z.string().optional(),
host: z.string(),
port: z.number(),
});

View File

@@ -0,0 +1,12 @@
import mqtt from "mqtt";
import type z from "zod";
import type { MQTTClientOptions } from "./mqtt-client.schema";
export async function getMqttClient(
options: z.output<typeof MQTTClientOptions>,
) {
return await mqtt.connectAsync(`mqtt://${options.host}:${options.port}`, {
password: options.password,
username: options.user,
});
}

View File

@@ -0,0 +1,99 @@
import z from "zod";
const ResponseBody = z.object({
code: z.number(),
msg: z.string(),
data: z.any(),
});
const CODE_SUCCESS = z.literal(0);
export const NanoKVMClientOptions = z.object({
/** host of nanokvm */
host: z.string(),
/** the nanokvm username */
username: z.string(),
/** the nanokvm password */
password: z.string(),
/** password secret, optional.
* only needs to be set if a different secret is set on the nanokvm for more secure password encryption. */
passwordSecret: z.string().default("nanokvm-sipeed-2024"),
/** https isn't usable by default so we use http */
protocol: z.enum(["http", "https"]).default("http"),
/** You can provide a token which can improve reliability */
token: z.string().optional(),
});
// POST - /api/auth/login
export const AuthLoginSchemaSuccess = ResponseBody.extend({
code: CODE_SUCCESS,
data: z.object({
token: z.string(),
}),
});
export const AuthLoginSchemaInvalidPassword = ResponseBody.extend({
code: z.literal(-2),
});
export const AuthLoginSchema = z.discriminatedUnion("code", [
AuthLoginSchemaSuccess,
AuthLoginSchemaInvalidPassword,
]);
// GET - /api/vm/info
export const InfoSchema = z.object({
ips: z.array(
z.object({
name: z.string(),
addr: z.string(),
version: z.string(),
type: z.string(),
}),
),
mdns: z.string(),
image: z.string(),
application: z.string(),
deviceKey: z.string(),
});
export const InfoSuccess = ResponseBody.extend({
code: CODE_SUCCESS,
data: InfoSchema,
});
// GET - /api/vm/gpio
export const GpioSchema = z.object({
pwr: z.boolean(),
hdd: z.boolean(),
});
export const GpioSuccess = ResponseBody.extend({
code: CODE_SUCCESS,
data: GpioSchema,
});
// POST - /api/vm/gpio
export const TriggerPowerError = ResponseBody.extend({
code: z.literal(-1),
data: z.null(),
});
export const TriggerPowerSuccess = ResponseBody.extend({
code: CODE_SUCCESS,
data: z.null(),
});
export const TriggerPowerSchema = z.discriminatedUnion("code", [
TriggerPowerSuccess,
TriggerPowerError,
]);
export const TriggerPowerInput = z.object({
type: z.literal("power").default("power"),
duration: z.number().min(0).max(20000).default(800),
});

View File

@@ -0,0 +1,90 @@
import type { BodyInit } from "bun";
import type z from "zod";
import { encryptPassword } from "@/encryption";
import { logger } from "@/logger";
import {
type AuthLoginSchema,
NanoKVMClientOptions,
} from "./nano-kvm-client.schema";
export class NanoKVMClient {
private baseUrl: URL;
private options: z.output<typeof NanoKVMClientOptions>;
private token: string | undefined;
constructor(options: z.input<typeof NanoKVMClientOptions>) {
this.options = NanoKVMClientOptions.parse(options);
this.baseUrl = new URL(`${this.options.protocol}://${this.options.host}`);
}
get authUrl() {
return new URL("/api/auth/login", this.baseUrl);
}
async init() {
if (this.options.token) {
this.token = this.options.token;
logger.info("sucessfully initialized nanokvm using token from config");
return;
}
this.loginAndSetToken();
}
async fetch(path: string, method?: "POST" | "GET", body?: BodyInit) {
return fetch(new URL(path, this.baseUrl), {
headers: {
cookie: `nano-kvm-token=${this.token}`,
},
body,
method,
});
}
/** Calls the login api and stores the returned token */
async loginAndSetToken() {
const response = await fetch(this.authUrl, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
username: this.options.username,
password: encryptPassword(
this.options.password,
this.options.passwordSecret,
),
}),
});
const result = (await response.json()) as z.output<
typeof AuthLoginSchema
> | null;
if (result === null) {
throw new Error("No response from api!");
}
switch (result.code) {
case 0: {
logger.debug(`Got token ${result.data.token}`);
this.token = result.data.token;
return;
}
case -2: {
logger.fatal(`Invalid password! Failed to get token.`);
return;
}
default: {
logger.fatal(
"Unknown error while getting token.",
(result as z.output<typeof AuthLoginSchema>).msg,
);
}
}
}
}

View File

@@ -0,0 +1,64 @@
import type z from "zod";
import { NanoKVMClient } from "./nano-kvm-client";
import {
type GpioSchema,
GpioSuccess,
type InfoSchema,
InfoSuccess,
type NanoKVMClientOptions,
TriggerPowerInput,
TriggerPowerSchema,
} from "./nano-kvm-client.schema";
export class NanoKVMService {
private client: NanoKVMClient;
constructor(options: {
clientOptions: z.input<typeof NanoKVMClientOptions>;
}) {
this.client = new NanoKVMClient(options.clientOptions);
}
async init() {
await this.client.init();
}
async getInfo(): Promise<z.output<typeof InfoSchema>> {
const data = await (await this.client.fetch("/api/vm/info", "GET")).json();
return InfoSuccess.parse(data).data;
}
async getGpio(): Promise<z.output<typeof GpioSchema>> {
const response = await this.client.fetch("/api/vm/gpio", "GET");
const data = await response.json();
return GpioSuccess.parse(data).data;
}
async triggerPower(input: z.input<typeof TriggerPowerInput>) {
const parsedInput = TriggerPowerInput.parse(input);
console.log(parsedInput);
const body = new FormData();
body.append("Type", parsedInput.type);
body.append("Duration", parsedInput.duration.toString());
const response = await this.client.fetch("/api/vm/gpio", "POST", body);
const data = await response.json();
const parsed = TriggerPowerSchema.parse(data);
return parsed;
}
async triggerReset() {
const result = await this.triggerPower({ duration: 8000 });
return result;
}
}

9
src/config.schema.ts Normal file
View File

@@ -0,0 +1,9 @@
import z from "zod";
import { MQTTClientOptions } from "./clients/mqtt/mqtt-client.schema";
import { NanoKVMClientOptions } from "./clients/nano-kvm/nano-kvm-client.schema";
export const ConfigSchema = z.object({
nanoKvm: NanoKVMClientOptions,
mqtt: MQTTClientOptions,
publishInterval: z.number().default(2000),
});

47
src/encryption.ts Normal file
View File

@@ -0,0 +1,47 @@
import crypto from "node:crypto";
export const encryptPassword = (password: string, passphrase: string) => {
// OpenSSL's default salt is 8 bytes
const salt = crypto.randomBytes(8);
// Derive key and IV using MD5 (matching OpenSSL's -md md5)
// OpenSSL uses EVP_BytesToKey algorithm
const keyIv = evpBytesToKey(passphrase, salt, 32, 16); // 32 bytes key, 16 bytes IV for AES-256
// Create cipher
const cipher = crypto.createCipheriv("aes-256-cbc", keyIv.key, keyIv.iv);
// Encrypt the password
let encrypted = cipher.update(password, "utf8");
encrypted = Buffer.concat([encrypted, cipher.final()]);
// OpenSSL prepends "Salted__" + salt to the encrypted data
const saltedPrefix = Buffer.from("Salted__", "utf8");
const result = Buffer.concat([saltedPrefix, salt, encrypted]);
// Return base64 encoded result
return result.toString("base64");
};
// EVP_BytesToKey implementation (OpenSSL's key derivation)
const evpBytesToKey = (
password: string,
salt: Buffer,
keyLen: number,
ivLen: number,
) => {
const md5 = (data: Buffer) => crypto.createHash("md5").update(data).digest();
let derived = Buffer.alloc(0);
let hash = Buffer.alloc(0);
while (derived.length < keyLen + ivLen) {
hash = md5(Buffer.concat([hash, Buffer.from(password, "utf8"), salt]));
derived = Buffer.concat([derived, hash]);
}
return {
key: derived.slice(0, keyLen),
iv: derived.slice(keyLen, keyLen + ivLen),
};
};

68
src/index.ts Normal file
View File

@@ -0,0 +1,68 @@
import { readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import { $, Glob } from "bun";
import { getMqttClient } from "./clients/mqtt/mqtt-client";
import { NanoKVMService } from "./clients/nano-kvm/nano-kvm.service";
import { ConfigSchema } from "./config.schema";
import { startListeners } from "./listener";
import { logger } from "./logger";
import { runPublishLoop } from "./publisher";
logger.info("Starting intialization...");
let hasMultipleConfigs = false;
const validConfigs = [];
try {
const glob = new Glob("*.client.json");
const configsFolder = process.env.CONFIGS_PATH || "./";
for await (const fileName of glob.scan(configsFolder)) {
logger.info(`Found client config ${fileName}, validating...`);
const fileContent = await readFile(join(configsFolder, fileName), {
encoding: "utf8",
}).catch((e) => {
throw new Error("Error while reading config!", { cause: e });
});
const json = JSON.parse(fileContent);
const result = ConfigSchema.safeParse(json);
if (result.success) {
validConfigs.push(result.data);
}
if (result.error) {
logger.fatal(result.error);
}
}
if (validConfigs.length > 0) {
hasMultipleConfigs = true;
}
throw new Error("No valid config found! Exiting...");
} catch {}
logger.info(
hasMultipleConfigs
? `Successfully loaded ${validConfigs.length} configs!`
: "Single config mode",
);
for (const config of validConfigs) {
const nanoKvmService = new NanoKVMService({ clientOptions: config.nanoKvm });
await nanoKvmService.init();
const info = await nanoKvmService.getInfo();
const mqttClient = await getMqttClient(config.mqtt);
runPublishLoop(info, nanoKvmService, config, mqttClient);
startListeners(info, nanoKvmService, config, mqttClient);
}

40
src/listener.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { MqttClient } from "mqtt";
import type z from "zod";
import type { NanoKVMService } from "./clients/nano-kvm/nano-kvm.service";
import type { InfoSchema } from "./clients/nano-kvm/nano-kvm-client.schema";
import type { ConfigSchema } from "./config.schema";
import { logger } from "./logger";
export async function startListeners(
info: z.output<typeof InfoSchema>,
nanoKvmService: NanoKVMService,
_config: z.output<typeof ConfigSchema>,
mqttClient: MqttClient,
) {
await mqttClient.subscribeAsync(`nanokvm-mqtt/${info.deviceKey}/+`);
mqttClient.on("message", async (topic, payload) => {
logger.info(topic, payload);
const pathParts = topic.split("/");
const action = pathParts[pathParts.length - 1];
switch (action) {
case "trigger-power": {
await nanoKvmService.triggerPower({});
return;
}
case "trigger-reset": {
await nanoKvmService.triggerReset();
return;
}
default: {
throw new Error(`Unhandled action: ${action}`);
}
}
});
}

5
src/logger.ts Normal file
View File

@@ -0,0 +1,5 @@
import { Logger } from "tslog";
export const logger = new Logger({
name: "nanokvm-mqtt",
});

128
src/publisher.ts Normal file
View File

@@ -0,0 +1,128 @@
import { sleep } from "bun";
import type { MqttClient } from "mqtt";
import type z from "zod";
import type { NanoKVMService } from "./clients/nano-kvm/nano-kvm.service";
import type { InfoSchema } from "./clients/nano-kvm/nano-kvm-client.schema";
import type { ConfigSchema } from "./config.schema";
import { logger } from "./logger";
export async function runPublishLoop(
info: z.output<typeof InfoSchema>,
nanoKvmService: NanoKVMService,
config: z.output<typeof ConfigSchema>,
mqttClient: MqttClient,
) {
while (true) {
const entityIds = {
gpio: {
hdd: "gpio_hdd",
power: "gpio_power",
},
actions: {
triggerPower: "action_trigger_power",
triggerReset: "action_trigger_reset",
},
};
const origin = {
name: "nanokvm-mqtt",
sw: "0.0.1",
url: "https://github.com/enricobuehler/nanokvm-mqtt",
};
const state_topic = `nanokvm-mqtt/${info.deviceKey}`;
const device = {
hw_version: 0,
identifiers: [info.deviceKey],
manufacturer: "Sipeed",
model: "Sipeed NanoKVM",
model_id: "unknown",
name: info.mdns,
sw_version: info.application,
};
// State
await mqttClient.publishAsync(
`homeassistant/binary_sensor/${info.deviceKey}/power/config`,
JSON.stringify({
default_entity_id: `binary_sensor.${info.deviceKey}_${entityIds.gpio.power}`,
device,
device_class: "power",
object_id: `nanokvm_${info.deviceKey}`,
origin,
state_topic,
payload_off: false,
payload_on: true,
unique_id: `${info.deviceKey}_nanokvm_${entityIds.gpio.power}`,
value_template: "{{ value_json.power }}",
}),
);
await mqttClient.publishAsync(
`homeassistant/binary_sensor/${info.deviceKey}/hdd/config`,
JSON.stringify({
default_entity_id: `binary_sensor.${info.deviceKey}_${entityIds.gpio.hdd}`,
device,
name: "HDD Activity",
object_id: `nanokvm_${info.deviceKey}_${entityIds.gpio.hdd}`,
origin,
state_topic,
payload_off: false,
payload_on: true,
unique_id: `${info.deviceKey}_nanokvm_${entityIds.gpio.hdd}`,
value_template: "{{ value_json.hdd }}",
}),
);
logger.debug("AutoDiscovery - Published states");
// Actions
await mqttClient.publishAsync(
`homeassistant/button/${info.deviceKey}/button-trigger-power/config`,
JSON.stringify({
device,
command_topic: `${state_topic}/trigger-power`,
default_entity_id: `button.${info.deviceKey}_${entityIds.actions.triggerPower}`,
name: "Trigger Power",
object_id: `nanokvm_${info.deviceKey}_${entityIds.actions.triggerPower}`,
payload_press: "trigger_power",
unique_id: `${info.deviceKey}_nanokvm_${entityIds.actions.triggerPower}`,
}),
);
await mqttClient.publishAsync(
`homeassistant/button/${info.deviceKey}/button-trigger-reset/config`,
JSON.stringify({
device,
command_topic: `${state_topic}/trigger-reset`,
default_entity_id: `button.${info.deviceKey}_${entityIds.actions.triggerReset}`,
name: "Trigger Reset",
object_id: `nanokvm_${info.deviceKey}_${entityIds.actions.triggerReset}`,
payload_press: "trigger_reset",
unique_id: `${info.deviceKey}_nanokvm_${entityIds.actions.triggerReset}`,
}),
);
//
logger.debug("AutoDiscovery - Published actions");
const gpio = await nanoKvmService.getGpio();
await mqttClient.publishAsync(
`nanokvm-mqtt/${info.deviceKey}`,
JSON.stringify({
power: gpio.pwr,
hdd: gpio.hdd,
}),
);
//console.log(info.ips);
//console.log(gpio);
await sleep(config.publishInterval);
}
}

View File

@@ -0,0 +1,7 @@
import { writeFile } from "node:fs/promises";
import z from "zod";
import { ConfigSchema } from "@/config.schema";
const jsonSchema = z.toJSONSchema(ConfigSchema);
await writeFile("./config-schema.json", JSON.stringify(jsonSchema));

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}

6
tsdown.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { defineConfig } from "tsdown";
export default defineConfig({
entry: "src/index.ts",
external: ["bun"],
});