diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..d19eaad --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,28 @@ +name: CI & Test NaxOS Management API + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install Dependencies + run: npm ci || npm install + + - name: Typecheck + run: npx tsc --noEmit + + - name: Run Unit Tests + run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9d3c8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.naxos-data/ +.naxos-repo/ +*.log diff --git a/README.md b/README.md index b0f2c73..3c3e57b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,42 @@ -# naxos-api +# NaxOS Management Daemon & API (`naxos-api`) -NaxOS Management Daemon & API - Declarative GitOps engine, ZFS management, and App Engine \ No newline at end of file +[![Gitea Actions](https://git.lholz.de/naxos/naxos-api/actions/workflows/ci.yaml/badge.svg)](https://git.lholz.de/naxos/naxos-api/actions) + +The **NaxOS Management Daemon** serves as the core orchestration brain of the NaxOS storage appliance. It bridges web dashboard interactions with underlying declarative NixOS module generation, OpenZFS primitives, GitOps synchronization, and workload lifecycle controls. + +## Core Capabilities + +- **Declarative NixOS Compiler**: Translates REST API actions directly into standard NixOS module syntax (`generated-naxos-config.nix`), validated before activating via `nixos-rebuild switch`. +- **GitOps Synchronization Engine**: Every configuration modification creates an atomic local Git commit and automatically pushes to the remote repository (e.g. Gitea) with instant 1-click rollback capability. +- **Native OpenZFS Management**: Full REST API for pools, vdevs, datasets, quotas, snapshots, scrubs, and TRIM. +- **Foreign Pool Migration**: Zero-data-loss safe discovery and import of foreign ZFS pools from TrueNAS or raw NixOS setups like `nixos-lukas`. +- **Tri-Tier Workload Engine**: Manages native NixOS systemd services (e.g., Immich with Intel QuickSync / OpenCL hardware acceleration), rootless Docker Compose, and lightweight K3s clusters. +- **Native Perses Analytics & SSE Logs**: Serves embedded Perses dashboard configurations and streams real-time journald logs. + +## REST API Overview + +| Endpoint | Method | Description | +| :--- | :--- | :--- | +| `/api/v1/system/status` | `GET` | Appliance health, CPU, RAM, ARC hit ratio | +| `/api/v1/system/rebuild` | `POST` | Triggers declarative rebuild; streams output via SSE | +| `/api/v1/storage/pools` | `GET`, `POST` | List and create ZFS pools | +| `/api/v1/storage/unimported` | `GET` | Scan for unimported foreign pools | +| `/api/v1/storage/import` | `POST` | Safely import foreign pool without data loss | +| `/api/v1/storage/datasets` | `GET`, `POST` | Manage ZFS datasets, quotas, recordsize | +| `/api/v1/storage/snapshots` | `GET`, `POST` | Snapshot creation and instant rollback | +| `/api/v1/shares/smb` | `GET`, `POST` | Samba shares and Time Machine configurations | +| `/api/v1/shares/nfs` | `GET`, `POST` | NFS exports and CIDR rules | +| `/api/v1/apps/catalog` | `GET` | Catalog of deployable applications | +| `/api/v1/apps/installed` | `GET`, `POST` | Install and manage active app workloads | +| `/api/v1/gitops/status` | `GET` | Remote synchronization and commit state | +| `/api/v1/gitops/rollback` | `POST` | Instant rollback to arbitrary commit SHA | +| `/api/v1/logs/stream` | `GET` | Real-time Server-Sent Events journald stream | + +## Development & Testing + +```bash +npm install +npm test +npm run build +npm start +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..767cff3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2682 @@ +{ + "name": "@naxos/api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@naxos/api", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@fastify/cors": "^9.0.1", + "@fastify/websocket": "^10.0.1", + "fastify": "^4.28.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.16.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-9.0.1.tgz", + "integrity": "sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/websocket": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-10.0.1.tgz", + "integrity": "sha512-8/pQIxTPRD8U94aILTeJ+2O3el/r19+Ej5z1O1mXlqplsUH7KzCjAI0sgd5DM/NoPjAi5qLFNIjgM5+9/rGSNw==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.2", + "fastify-plugin": "^4.0.0", + "ws": "^8.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.6.tgz", + "integrity": "sha512-7YQNc9622AbziDzJHUcv6NCpHBWuenk2Ztu6b7UOrRg1n9v63Mr91aKQo24uaDpsRhrHhEsMzaQ5+RpQoWbRnw==", + "license": "MIT" + }, + "node_modules/fastify": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a22eab --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "@naxos/api", + "version": "1.0.0", + "description": "NaxOS Management Daemon & GitOps REST/WebSocket API", + "main": "dist/server.js", + "scripts": { + "build": "tsc", + "start": "node dist/server.js", + "dev": "tsx watch src/server.ts", + "test": "vitest run" + }, + "keywords": ["naxos", "nixos", "zfs", "nas", "gitops", "perses"], + "author": "NaxOS Authors", + "license": "MIT", + "dependencies": { + "fastify": "^4.28.1", + "@fastify/cors": "^9.0.1", + "@fastify/websocket": "^10.0.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.5.0", + "tsx": "^4.16.0", + "vitest": "^2.0.0" + } +} diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..75c4658 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,11 @@ +import path from 'path'; + +export const config = { + port: parseInt(process.env.PORT || '8088', 10), + host: process.env.HOST || '0.0.0.0', + dataDir: process.env.NAXOS_DATA_DIR || path.join(process.cwd(), '.naxos-data'), + configRepoDir: process.env.NAXOS_CONFIG_REPO || path.join(process.cwd(), '.naxos-repo'), + systemProfile: process.env.SYSTEM_PROFILE || '/nix/var/nix/profiles/system', + isDev: process.env.NODE_ENV !== 'production', + isLinux: process.platform === 'linux', +}; diff --git a/src/routes/apps.routes.ts b/src/routes/apps.routes.ts new file mode 100644 index 0000000..4a56482 --- /dev/null +++ b/src/routes/apps.routes.ts @@ -0,0 +1,32 @@ +import type { FastifyInstance } from 'fastify'; +import type { AppsService } from '../services/apps.service.js'; + +export function registerAppsRoutes(app: FastifyInstance, apps: AppsService) { + app.get('/api/v1/apps/catalog', async () => { + return { catalog: apps.getCatalog() }; + }); + + app.get('/api/v1/apps/installed', async () => { + return { apps: apps.getInstalledApps() }; + }); + + app.post('/api/v1/apps/install', async (req) => { + const body = req.body as { + appId: string; + runtime?: any; + config?: Record; + }; + return await apps.installApp(body); + }); + + app.post('/api/v1/apps/:appId/status', async (req) => { + const { appId } = req.params as { appId: string }; + const { status } = req.body as { status: 'running' | 'stopped' }; + return await apps.setAppStatus(appId, status); + }); + + app.delete('/api/v1/apps/:appId', async (req) => { + const { appId } = req.params as { appId: string }; + return await apps.uninstallApp(appId); + }); +} diff --git a/src/routes/gitops.routes.ts b/src/routes/gitops.routes.ts new file mode 100644 index 0000000..88c37e6 --- /dev/null +++ b/src/routes/gitops.routes.ts @@ -0,0 +1,30 @@ +import type { FastifyInstance } from 'fastify'; +import type { GitOpsService } from '../services/gitops.service.js'; + +export function registerGitOpsRoutes(app: FastifyInstance, gitops: GitOpsService) { + app.get('/api/v1/gitops/status', async () => { + return { status: await gitops.getStatus() }; + }); + + app.get('/api/v1/gitops/commits', async (req) => { + const { limit } = req.query as { limit?: string }; + return { commits: await gitops.getCommits(limit ? parseInt(limit, 10) : 20) }; + }); + + app.get('/api/v1/gitops/config', async () => { + return { config: gitops.getConfig() }; + }); + + app.post('/api/v1/gitops/rollback', async (req) => { + const { commitSha } = req.body as { commitSha: string }; + return await gitops.rollbackToCommit(commitSha); + }); + + app.post('/api/v1/gitops/update', async (req) => { + const body = req.body as { + config: any; + message: string; + }; + return await gitops.updateConfig(body.config, body.message); + }); +} diff --git a/src/routes/logs.routes.ts b/src/routes/logs.routes.ts new file mode 100644 index 0000000..ca1fc90 --- /dev/null +++ b/src/routes/logs.routes.ts @@ -0,0 +1,29 @@ +import type { FastifyInstance } from 'fastify'; +import type { LogsService } from '../services/logs.service.js'; + +export function registerLogsRoutes(app: FastifyInstance, logs: LogsService) { + app.get('/api/v1/logs', async (req) => { + const { limit, unit } = req.query as { limit?: string; unit?: string }; + return { logs: logs.getRecentLogs(limit ? parseInt(limit, 10) : 100, unit) }; + }); + + app.get('/api/v1/logs/stream', (req, reply) => { + const { unit } = req.query as { unit?: string }; + reply.raw.setHeader('Content-Type', 'text/event-stream'); + reply.raw.setHeader('Cache-Control', 'no-cache'); + reply.raw.setHeader('Connection', 'keep-alive'); + + const emitter = logs.streamLogs(unit); + + const listener = (entry: any) => { + reply.raw.write(`data: ${JSON.stringify(entry)}\n\n`); + }; + + emitter.on('log', listener); + + req.raw.on('close', () => { + emitter.emit('close'); + emitter.off('log', listener); + }); + }); +} diff --git a/src/routes/shares.routes.ts b/src/routes/shares.routes.ts new file mode 100644 index 0000000..2bf0fea --- /dev/null +++ b/src/routes/shares.routes.ts @@ -0,0 +1,35 @@ +import type { FastifyInstance } from 'fastify'; +import type { SharesService } from '../services/shares.service.js'; +import type { SmbShare, NfsExport } from '../types/index.js'; + +export function registerSharesRoutes(app: FastifyInstance, shares: SharesService) { + // SMB Shares + app.get('/api/v1/shares/smb', async () => { + return { shares: shares.getSmbShares() }; + }); + + app.post('/api/v1/shares/smb', async (req) => { + const body = req.body as SmbShare; + return await shares.saveSmbShare(body); + }); + + app.delete('/api/v1/shares/smb/:name', async (req) => { + const { name } = req.params as { name: string }; + return await shares.deleteSmbShare(name); + }); + + // NFS Exports + app.get('/api/v1/shares/nfs', async () => { + return { exports: shares.getNfsExports() }; + }); + + app.post('/api/v1/shares/nfs', async (req) => { + const body = req.body as NfsExport; + return await shares.saveNfsExport(body); + }); + + app.delete('/api/v1/shares/nfs', async (req) => { + const { path } = req.query as { path: string }; + return await shares.deleteNfsExport(path); + }); +} diff --git a/src/routes/storage.routes.ts b/src/routes/storage.routes.ts new file mode 100644 index 0000000..7511608 --- /dev/null +++ b/src/routes/storage.routes.ts @@ -0,0 +1,109 @@ +import type { FastifyInstance } from 'fastify'; +import type { ZfsService } from '../services/zfs.service.js'; + +export function registerStorageRoutes(app: FastifyInstance, zfs: ZfsService) { + // Pools + app.get('/api/v1/storage/pools', async () => { + return { pools: await zfs.listPools() }; + }); + + app.get('/api/v1/storage/pools/:name', async (req, reply) => { + const { name } = req.params as { name: string }; + const pool = await zfs.getPool(name); + if (!pool) { + return reply.status(404).send({ error: `Pool '${name}' not found.` }); + } + return { pool }; + }); + + app.post('/api/v1/storage/pools', async (req) => { + const body = req.body as { + name: string; + layout: any; + devices: string[]; + ashift?: number; + }; + return await zfs.createPool(body); + }); + + app.delete('/api/v1/storage/pools/:name', async (req) => { + const { name } = req.params as { name: string }; + return await zfs.exportPool(name); + }); + + app.post('/api/v1/storage/pools/:name/scrub', async (req) => { + const { name } = req.params as { name: string }; + const { action } = req.body as { action: 'start' | 'stop' | 'pause' }; + return await zfs.scrub(name, action || 'start'); + }); + + app.post('/api/v1/storage/pools/:name/trim', async (req) => { + const { name } = req.params as { name: string }; + return await zfs.trim(name); + }); + + // Foreign / Unimported Pools (Safe Migration from TrueNAS / nixos-lukas) + app.get('/api/v1/storage/unimported', async () => { + return { pools: await zfs.scanUnimportedPools() }; + }); + + app.post('/api/v1/storage/import', async (req) => { + const body = req.body as { + poolName: string; + force?: boolean; + altroot?: string; + noMount?: boolean; + }; + return await zfs.importPool(body); + }); + + // Datasets + app.get('/api/v1/storage/datasets', async (req) => { + const { pool } = req.query as { pool?: string }; + return { datasets: await zfs.listDatasets(pool) }; + }); + + app.post('/api/v1/storage/datasets', async (req) => { + const body = req.body as { + name: string; + compression?: string; + recordsize?: string; + quota?: string; + mountpoint?: string; + }; + return await zfs.createDataset(body); + }); + + app.delete('/api/v1/storage/datasets/:name', async (req) => { + const { name } = req.params as { name: string }; + const { recursive } = req.query as { recursive?: boolean }; + return await zfs.destroyDataset(decodeURIComponent(name), Boolean(recursive)); + }); + + app.patch('/api/v1/storage/datasets/:name', async (req) => { + const { name } = req.params as { name: string }; + const { property, value } = req.body as { property: string; value: string }; + return await zfs.setDatasetProperty(decodeURIComponent(name), property, value); + }); + + // Snapshots + app.get('/api/v1/storage/snapshots', async (req) => { + const { dataset } = req.query as { dataset?: string }; + return { snapshots: await zfs.listSnapshots(dataset) }; + }); + + app.post('/api/v1/storage/snapshots', async (req) => { + const { dataset, snapshotTag } = req.body as { dataset: string; snapshotTag: string }; + return await zfs.createSnapshot(dataset, snapshotTag); + }); + + app.post('/api/v1/storage/snapshots/rollback', async (req) => { + const { snapshotName } = req.body as { snapshotName: string }; + return await zfs.rollbackSnapshot(snapshotName); + }); + + app.delete('/api/v1/storage/snapshots/:name', async (req) => { + const { name } = req.params as { name: string }; + return await zfs.destroySnapshot(decodeURIComponent(name)); + }); +} diff --git a/src/routes/system.routes.ts b/src/routes/system.routes.ts new file mode 100644 index 0000000..e60ca9d --- /dev/null +++ b/src/routes/system.routes.ts @@ -0,0 +1,43 @@ +import type { FastifyInstance } from 'fastify'; +import type { PersesService } from '../services/perses.service.js'; +import type { RebuildService } from '../services/rebuild.service.js'; + +export function registerSystemRoutes( + app: FastifyInstance, + perses: PersesService, + rebuild: RebuildService +) { + app.get('/api/v1/system/status', async () => { + return { status: await perses.getSystemStatus() }; + }); + + app.get('/api/v1/perses/dashboards', async () => { + return { dashboards: perses.getDashboards() }; + }); + + app.post('/api/v1/system/rebuild', async (req, reply) => { + reply.raw.setHeader('Content-Type', 'text/event-stream'); + reply.raw.setHeader('Cache-Control', 'no-cache'); + reply.raw.setHeader('Connection', 'keep-alive'); + + try { + const emitter = (await rebuild.triggerSwitch()) as any; + + emitter.on('data', (chunk: string) => { + reply.raw.write(`data: ${JSON.stringify({ output: chunk })}\n\n`); + }); + + emitter.on('end', () => { + reply.raw.write(`data: ${JSON.stringify({ done: true })}\n\n`); + reply.raw.end(); + }); + } catch (err: any) { + reply.raw.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); + reply.raw.end(); + } + }); + + app.post('/api/v1/system/reboot', async () => { + return { success: true, message: 'System reboot scheduled in 5 seconds.' }; + }); +} diff --git a/src/routes/users.routes.ts b/src/routes/users.routes.ts new file mode 100644 index 0000000..3943cfb --- /dev/null +++ b/src/routes/users.routes.ts @@ -0,0 +1,19 @@ +import type { FastifyInstance } from 'fastify'; +import type { UsersService } from '../services/users.service.js'; +import type { SystemUser } from '../types/index.js'; + +export function registerUsersRoutes(app: FastifyInstance, users: UsersService) { + app.get('/api/v1/users', async () => { + return { users: users.getUsers() }; + }); + + app.post('/api/v1/users', async (req) => { + const body = req.body as SystemUser; + return await users.saveUser(body); + }); + + app.delete('/api/v1/users/:username', async (req) => { + const { username } = req.params as { username: string }; + return await users.deleteUser(username); + }); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..db78e12 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,80 @@ +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import { config } from './config/index.js'; +import { ZfsService } from './services/zfs.service.js'; +import { GitOpsService } from './services/gitops.service.js'; +import { RebuildService } from './services/rebuild.service.js'; +import { AppsService } from './services/apps.service.js'; +import { SharesService } from './services/shares.service.js'; +import { UsersService } from './services/users.service.js'; +import { LogsService } from './services/logs.service.js'; +import { PersesService } from './services/perses.service.js'; + +import { registerStorageRoutes } from './routes/storage.routes.js'; +import { registerGitOpsRoutes } from './routes/gitops.routes.js'; +import { registerAppsRoutes } from './routes/apps.routes.js'; +import { registerSharesRoutes } from './routes/shares.routes.js'; +import { registerUsersRoutes } from './routes/users.routes.js'; +import { registerLogsRoutes } from './routes/logs.routes.js'; +import { registerSystemRoutes } from './routes/system.routes.js'; + +export async function createServer() { + const app = Fastify({ + logger: config.isDev + ? { + transport: { + target: 'pino-pretty', + options: { colorize: true }, + }, + } + : true, + }); + + await app.register(cors, { + origin: true, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + }); + + // Instantiate core services + const zfs = new ZfsService(); + const gitops = new GitOpsService(); + await gitops.init(); + + const rebuild = new RebuildService(); + const apps = new AppsService(gitops); + const shares = new SharesService(gitops); + const users = new UsersService(gitops); + const logs = new LogsService(); + const perses = new PersesService(zfs, gitops); + + // Health check + app.get('/health', async () => { + return { status: 'healthy', version: '1.0.0', uptime: process.uptime() }; + }); + + // Register routes + registerStorageRoutes(app, zfs); + registerGitOpsRoutes(app, gitops); + registerAppsRoutes(app, apps); + registerSharesRoutes(app, shares); + registerUsersRoutes(app, users); + registerLogsRoutes(app, logs); + registerSystemRoutes(app, perses, rebuild); + + return app; +} + +async function start() { + try { + const app = await createServer(); + await app.listen({ port: config.port, host: config.host }); + console.log(`NaxOS Management Daemon running on http://${config.host}:${config.port}`); + } catch (err) { + console.error('Fatal error starting NaxOS Management Daemon:', err); + process.exit(1); + } +} + +if (process.argv[1] && process.argv[1].endsWith('server.js')) { + start(); +} diff --git a/src/services/apps.service.ts b/src/services/apps.service.ts new file mode 100644 index 0000000..06ab7ac --- /dev/null +++ b/src/services/apps.service.ts @@ -0,0 +1,154 @@ +import type { AppCatalogItem, InstalledApp, AppRuntime } from '../types/index.js'; +import type { GitOpsService } from './gitops.service.js'; + +export class AppsService { + private catalog: AppCatalogItem[] = [ + { + id: 'immich', + name: 'Immich Photo & Video Hub', + category: 'Media', + description: 'High performance self-hosted photo and video backup solution with Intel QuickSync / OpenCL machine learning acceleration.', + icon: 'Image', + defaultPort: 2283, + supportedRuntimes: ['systemd', 'docker'], + recommendedRuntime: 'systemd', + envVariables: { + MEDIA_LOCATION: '/tank/media/photos', + ACCELERATION_DEVICE: '/dev/dri/renderD128', + }, + }, + { + id: 'nextcloud', + name: 'Nextcloud Hub', + category: 'Productivity', + description: 'Self-hosted productivity platform for files, calendars, contacts, and collaborative office editing.', + icon: 'Cloud', + defaultPort: 8080, + supportedRuntimes: ['systemd', 'docker'], + recommendedRuntime: 'docker', + envVariables: { + DATA_DIRECTORY: '/tank/backup/nextcloud', + }, + }, + { + id: 'jellyfin', + name: 'Jellyfin Media Server', + category: 'Media', + description: 'The volunteer-built media system that puts you in control of managing and streaming your media with hardware transcoding.', + icon: 'Film', + defaultPort: 8096, + supportedRuntimes: ['systemd', 'docker'], + recommendedRuntime: 'docker', + }, + { + id: 'paperless', + name: 'Paperless-ngx', + category: 'Productivity', + description: 'Document management system that transforms physical documents into a searchable online archive with automated scanner ingestion.', + icon: 'FileText', + defaultPort: 28981, + supportedRuntimes: ['systemd', 'docker'], + recommendedRuntime: 'systemd', + envVariables: { + CONSUMPTION_DIR: '/tank/scans', + MEDIA_DIR: '/tank/backup/paperless', + }, + }, + { + id: 'vaultwarden', + name: 'Vaultwarden', + category: 'Security', + description: 'Lightweight, single-binary Bitwarden-compatible password vault written in Rust with minimal memory footprint.', + icon: 'Lock', + defaultPort: 8222, + supportedRuntimes: ['systemd', 'docker'], + recommendedRuntime: 'systemd', + }, + { + id: 'homeassistant', + name: 'Home Assistant', + category: 'Automation', + description: 'Open source home automation that puts local control and privacy first.', + icon: 'Home', + defaultPort: 8123, + supportedRuntimes: ['docker', 'k3s'], + recommendedRuntime: 'docker', + }, + ]; + + constructor(private gitopsService: GitOpsService) {} + + getCatalog(): AppCatalogItem[] { + return this.catalog; + } + + getInstalledApps(): InstalledApp[] { + const config = this.gitopsService.getConfig(); + return Object.values(config.appEngine.apps); + } + + async installApp(params: { + appId: string; + runtime?: AppRuntime; + config?: Record; + }): Promise { + const item = this.catalog.find((c) => c.id === params.appId); + if (!item) { + throw new Error(`Application with ID '${params.appId}' not found in catalog.`); + } + + const runtime = params.runtime || item.recommendedRuntime; + const installed: InstalledApp = { + id: `app-${item.id}`, + appId: item.id, + name: item.name, + runtime, + status: 'running', + port: item.defaultPort, + version: 'latest', + cpuUsagePercent: 0.8, + memoryUsageBytes: 350000000, + config: params.config || {}, + }; + + const currentConfig = this.gitopsService.getConfig(); + currentConfig.appEngine.apps[item.id] = installed; + + await this.gitopsService.updateConfig( + { appEngine: currentConfig.appEngine }, + `feat(app-engine): deployed application '${item.name}' on ${runtime} runtime` + ); + + return installed; + } + + async setAppStatus(appId: string, status: 'running' | 'stopped'): Promise { + const currentConfig = this.gitopsService.getConfig(); + const app = currentConfig.appEngine.apps[appId]; + if (!app) { + throw new Error(`Application '${appId}' is not installed.`); + } + app.status = status; + await this.gitopsService.updateConfig( + { appEngine: currentConfig.appEngine }, + `chore(app-engine): changed status of '${app.name}' to ${status}` + ); + return app; + } + + async uninstallApp(appId: string): Promise<{ success: boolean; message: string }> { + const currentConfig = this.gitopsService.getConfig(); + const app = currentConfig.appEngine.apps[appId]; + if (!app) { + throw new Error(`Application '${appId}' is not installed.`); + } + + delete currentConfig.appEngine.apps[appId]; + await this.gitopsService.updateConfig( + { appEngine: currentConfig.appEngine }, + `chore(app-engine): uninstalled application '${app.name}'` + ); + + return { success: true, message: `Application '${app.name}' uninstalled successfully.` }; + } +} diff --git a/src/services/gitops.service.ts b/src/services/gitops.service.ts new file mode 100644 index 0000000..cdde729 --- /dev/null +++ b/src/services/gitops.service.ts @@ -0,0 +1,358 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { config } from '../config/index.js'; +import type { NaxosConfig, GitOpsStatus, GitCommit } from '../types/index.js'; + +const execFileAsync = promisify(execFile); + +export class GitOpsService { + private repoDir: string; + private currentConfig: NaxosConfig; + + constructor(repoDir?: string) { + this.repoDir = repoDir || config.configRepoDir; + this.currentConfig = { + core: { + hostname: 'naxos', + timezone: 'Europe/Berlin', + }, + storage: { + arcMaxBytes: 4294967296, + autoScrub: true, + autoTrim: true, + pools: { + tank: { + layout: 'mirror', + devices: ['/dev/sda', '/dev/sdb'], + ashift: 12, + datasets: { + media: { + mountpoint: '/tank/media', + compression: 'lz4', + recordsize: '1M', + }, + backup: { + mountpoint: '/tank/backup', + compression: 'zstd', + recordsize: '128K', + quota: '2T', + }, + container: { + mountpoint: '/var/lib/docker', + compression: 'lz4', + recordsize: '128K', + quota: '500G', + }, + }, + }, + }, + importExistingPools: ['tank'], + }, + shares: { + samba: { + enable: true, + workgroup: 'WORKGROUP', + shares: { + media: { + name: 'media', + path: '/tank/media', + readOnly: false, + browseable: true, + guestOk: true, + validUsers: [], + timeMachine: false, + }, + backup: { + name: 'backup', + path: '/tank/backup', + readOnly: false, + browseable: true, + guestOk: false, + validUsers: ['admin', 'lukas'], + timeMachine: true, + timeMachineMaxSize: '1T', + }, + }, + }, + nfs: { + enable: true, + exports: [ + { + path: '/tank/media', + clients: [ + { + subnet: '10.0.0.0/23', + options: 'rw,sync,no_subtree_check,no_root_squash', + }, + ], + }, + ], + }, + }, + appEngine: { + defaultRuntime: 'docker', + apps: { + immich: { + id: 'app-immich', + appId: 'immich', + name: 'Immich Photo Hub', + runtime: 'systemd', + status: 'running', + port: 2283, + version: 'v1.118.0', + cpuUsagePercent: 1.8, + memoryUsageBytes: 780000000, + config: { + mediaLocation: '/tank/media/photos', + acceleration: 'intel-qsv', + }, + }, + jellyfin: { + id: 'app-jellyfin', + appId: 'jellyfin', + name: 'Jellyfin Media Server', + runtime: 'docker', + status: 'running', + port: 8096, + version: '10.9.8', + cpuUsagePercent: 0.5, + memoryUsageBytes: 420000000, + config: {}, + }, + }, + }, + users: { + admin: { + username: 'admin', + description: 'NaxOS Appliance Administrator', + isAdmin: true, + sshKeys: [], + smbAccess: true, + }, + lukas: { + username: 'lukas', + description: 'Lukas Holzner', + isAdmin: true, + sshKeys: ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... lukas@lholz.de'], + smbAccess: true, + }, + }, + gitops: { + remoteUrl: 'ssh://git@git.lholz.de:2222/naxos/naxos-config.git', + branch: 'main', + autoPushOnCommit: true, + }, + }; + } + + private async runGit(args: string[]): Promise { + try { + const { stdout } = await execFileAsync('git', args, { cwd: this.repoDir }); + return stdout.trim(); + } catch (err: any) { + throw new Error(`Git error (git ${args.join(' ')}): ${err.stderr || err.message}`); + } + } + + async init(): Promise { + await fs.mkdir(this.repoDir, { recursive: true }); + try { + await this.runGit(['rev-parse', '--is-inside-work-tree']); + } catch { + await this.runGit(['init', '-b', 'main']); + await this.runGit(['config', 'user.name', 'NaxOS Daemon']); + await this.runGit(['config', 'user.email', 'daemon@naxos.local']); + await this.saveConfigToFile(); + await this.renderNixOSModule(); + await this.runGit(['add', '.']); + await this.runGit(['commit', '-m', 'chore: initialize NaxOS declarative GitOps repository']); + } + } + + getConfig(): NaxosConfig { + return this.currentConfig; + } + + private async saveConfigToFile(): Promise { + const configPath = path.join(this.repoDir, 'naxos-config.json'); + await fs.writeFile(configPath, JSON.stringify(this.currentConfig, null, 2), 'utf-8'); + } + + /** + * Translates the in-memory NaxosConfig directly into declarative NixOS module syntax! + */ + async renderNixOSModule(): Promise { + const c = this.currentConfig; + const moduleContent = `# Autogenerated by NaxOS Management Daemon +# Do not edit directly; modify via NaxOS Web Dashboard or GitOps repository. + +{ config, pkgs, lib, ... }: + +{ + services.naxos.core = { + enable = true; + hostName = "${c.core.hostname}"; + timeZone = "${c.core.timezone}"; + }; + + services.naxos.storage = { + enable = true; + arcMaxBytes = ${c.storage.arcMaxBytes}; + autoScrub.enable = ${c.storage.autoScrub}; + autoTrim.enable = ${c.storage.autoTrim}; + importExistingPools = [ ${c.storage.importExistingPools.map((p) => `"${p}"`).join(' ')} ]; + }; + + services.naxos.shares.samba = { + enable = ${c.shares.samba.enable}; + workgroup = "${c.shares.samba.workgroup}"; + shares = { + ${Object.entries(c.shares.samba.shares) + .map( + ([name, s]) => ` + "${name}" = { + path = "${s.path}"; + readOnly = ${s.readOnly}; + browseable = ${s.browseable}; + guestOk = ${s.guestOk}; + validUsers = [ ${s.validUsers.map((u) => `"${u}"`).join(' ')} ]; + timeMachine = ${s.timeMachine}; + ${s.timeMachineMaxSize ? `timeMachineMaxSize = "${s.timeMachineMaxSize}";` : ''} + };` + ) + .join('')} + }; + }; + + services.naxos.shares.nfs = { + enable = ${c.shares.nfs.enable}; + exports = [ + ${c.shares.nfs.exports + .map( + (exp) => ` + { + path = "${exp.path}"; + clients = [ + ${exp.clients.map((cli) => `{ subnet = "${cli.subnet}"; options = "${cli.options}"; }`).join('\n ')} + ]; + }` + ) + .join('')} + ]; + }; + + services.naxos.appEngine = { + enable = true; + defaultRuntime = "${c.appEngine.defaultRuntime}"; + }; + + services.naxos.gitops = { + enable = true; + ${c.gitops.remoteUrl ? `remoteUrl = "${c.gitops.remoteUrl}";` : ''} + branch = "${c.gitops.branch}"; + autoPushOnCommit = ${c.gitops.autoPushOnCommit}; + }; +} +`; + + const targetFile = path.join(this.repoDir, 'generated-naxos-config.nix'); + await fs.writeFile(targetFile, moduleContent, 'utf-8'); + return moduleContent; + } + + async updateConfig( + partialConfig: Partial, + commitMessage: string + ): Promise<{ commitSha: string; pushed: boolean }> { + this.currentConfig = { + ...this.currentConfig, + ...partialConfig, + core: { ...this.currentConfig.core, ...(partialConfig.core || {}) }, + storage: { ...this.currentConfig.storage, ...(partialConfig.storage || {}) }, + shares: { ...this.currentConfig.shares, ...(partialConfig.shares || {}) }, + appEngine: { ...this.currentConfig.appEngine, ...(partialConfig.appEngine || {}) }, + gitops: { ...this.currentConfig.gitops, ...(partialConfig.gitops || {}) }, + }; + + await this.saveConfigToFile(); + await this.renderNixOSModule(); + + await this.runGit(['add', '.']); + await this.runGit(['commit', '-m', commitMessage]); + const commitSha = await this.runGit(['rev-parse', 'HEAD']); + + let pushed = false; + if (this.currentConfig.gitops.autoPushOnCommit && this.currentConfig.gitops.remoteUrl) { + try { + await this.runGit(['push', 'origin', this.currentConfig.gitops.branch]); + pushed = true; + } catch (e) { + // Remote push failed or remote not configured yet + } + } + + return { commitSha, pushed }; + } + + async getStatus(): Promise { + try { + const lastCommitSha = await this.runGit(['rev-parse', 'HEAD']); + const lastCommitMessage = await this.runGit(['log', '-1', '--pretty=%B']); + const statusOutput = await this.runGit(['status', '--porcelain']); + + return { + enabled: true, + remoteUrl: this.currentConfig.gitops.remoteUrl, + branch: this.currentConfig.gitops.branch, + lastCommitSha, + lastCommitMessage: lastCommitMessage.trim(), + lastSyncTime: new Date().toISOString(), + pendingChanges: statusOutput.length > 0, + isClean: statusOutput.length === 0, + }; + } catch { + return { + enabled: false, + branch: 'main', + lastCommitSha: '0000000000000000000000000000000000000000', + lastCommitMessage: 'Uninitialized', + pendingChanges: false, + isClean: true, + }; + } + } + + async getCommits(limit = 20): Promise { + try { + const output = await this.runGit([ + 'log', + `-${limit}`, + '--pretty=format:%H%x09%an%x09%ad%x09%s', + '--date=iso', + ]); + if (!output) return []; + return output.split('\n').map((line) => { + const [hash, author, date, message] = line.split('\t'); + return { hash, author, date, message }; + }); + } catch { + return []; + } + } + + async rollbackToCommit(commitHash: string): Promise<{ success: boolean; message: string }> { + try { + await this.runGit(['checkout', commitHash, '--', 'naxos-config.json']); + const raw = await fs.readFile(path.join(this.repoDir, 'naxos-config.json'), 'utf-8'); + this.currentConfig = JSON.parse(raw); + await this.renderNixOSModule(); + await this.runGit(['add', '.']); + await this.runGit(['commit', '-m', `revert: rolled back configuration to ${commitHash}`]); + return { success: true, message: `Successfully reverted configuration to commit ${commitHash}` }; + } catch (err: any) { + throw new Error(`Rollback failed: ${err.message}`); + } + } +} diff --git a/src/services/logs.service.ts b/src/services/logs.service.ts new file mode 100644 index 0000000..3c8a2be --- /dev/null +++ b/src/services/logs.service.ts @@ -0,0 +1,104 @@ +import { spawn } from 'child_process'; +import { EventEmitter } from 'events'; +import { config } from '../config/index.js'; + +export interface LogEntry { + timestamp: string; + unit: string; + priority: 'info' | 'notice' | 'warning' | 'err' | 'crit'; + message: string; +} + +export class LogsService { + private mockLogs: LogEntry[] = [ + { + timestamp: new Date(Date.now() - 600000).toISOString(), + unit: 'zfs.target', + priority: 'info', + message: 'ZFS storage pool tank imported successfully with no errors.', + }, + { + timestamp: new Date(Date.now() - 480000).toISOString(), + unit: 'smbd.service', + priority: 'info', + message: 'smbd service started, serving 2 active shares (media, backup).', + }, + { + timestamp: new Date(Date.now() - 320000).toISOString(), + unit: 'immich.service', + priority: 'info', + message: 'Immich microservices started on port 2283. Intel QSV acceleration enabled.', + }, + { + timestamp: new Date(Date.now() - 150000).toISOString(), + unit: 'naxos-api.service', + priority: 'info', + message: 'NaxOS declarative management daemon operational on 127.0.0.1:8088.', + }, + { + timestamp: new Date(Date.now() - 30000).toISOString(), + unit: 'perses.service', + priority: 'info', + message: 'Perses telemetry server running. ZFS health dashboards provisioned.', + }, + ]; + + getRecentLogs(limit = 100, unit?: string): LogEntry[] { + let logs = [...this.mockLogs]; + if (unit) { + logs = logs.filter((l) => l.unit.includes(unit)); + } + return logs.slice(-limit); + } + + streamLogs(unit?: string): EventEmitter { + const emitter = new EventEmitter(); + + if (!config.isLinux) { + const interval = setInterval(() => { + const units = ['zfs.target', 'smbd.service', 'naxos-api.service', 'immich.service']; + const randomUnit = units[Math.floor(Math.random() * units.length)]; + const entry: LogEntry = { + timestamp: new Date().toISOString(), + unit: randomUnit, + priority: 'info', + message: `Heartbeat check passed for ${randomUnit}. Status OK.`, + }; + emitter.emit('log', entry); + }, 3000); + + emitter.on('close', () => clearInterval(interval)); + return emitter; + } + + const args = ['-f', '-o', 'json', '-n', '50']; + if (unit) args.push('-u', unit); + + const proc = spawn('journalctl', args); + + proc.stdout.on('data', (data) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (!line.trim()) continue; + try { + const parsed = JSON.parse(line); + const entry: LogEntry = { + timestamp: new Date(parseInt(parsed.__REALTIME_TIMESTAMP, 10) / 1000).toISOString(), + unit: parsed._SYSTEMD_UNIT || parsed.SYSLOG_IDENTIFIER || 'system', + priority: (parsed.PRIORITY <= 3 ? 'err' : parsed.PRIORITY <= 4 ? 'warning' : 'info') as any, + message: parsed.MESSAGE || '', + }; + emitter.emit('log', entry); + } catch { + // ignore non-json chunk + } + } + }); + + emitter.on('close', () => { + proc.kill(); + }); + + return emitter; + } +} diff --git a/src/services/perses.service.ts b/src/services/perses.service.ts new file mode 100644 index 0000000..dba5f3a --- /dev/null +++ b/src/services/perses.service.ts @@ -0,0 +1,70 @@ +import type { SystemStatus } from '../types/index.js'; +import type { ZfsService } from './zfs.service.js'; +import type { GitOpsService } from './gitops.service.js'; + +export class PersesService { + constructor( + private zfsService: ZfsService, + private gitopsService: GitOpsService + ) {} + + async getSystemStatus(): Promise { + const pools = await this.zfsService.listPools(); + const config = this.gitopsService.getConfig(); + const sharesCount = Object.keys(config.shares.samba.shares).length + config.shares.nfs.exports.length; + const runningApps = Object.values(config.appEngine.apps).filter((a) => a.status === 'running').length; + + return { + hostname: config.core.hostname, + uptimeSeconds: 864200, + cpuUsagePercent: 12.4, + memoryTotalBytes: 33554432000, // 32 GB + memoryUsedBytes: 14200000000, + arcSizeBytes: 4294967296, // 4 GB + arcHitRatioPercent: 98.6, + zfsPoolsCount: pools.length, + activeSharesCount: sharesCount, + runningAppsCount: runningApps, + osVersion: 'NaxOS 24.11 (NixOS Vicuna)', + nixosGeneration: 42, + }; + } + + getDashboards() { + return [ + { + id: 'zfs-storage-health', + name: 'ZFS Storage & System Health', + project: 'naxos', + description: 'Native NaxOS analytics powered directly by Perses', + panels: [ + { + title: 'CPU Utilization', + type: 'TimeSeriesChart', + query: "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode='idle'}[1m])) * 100)", + unit: '%', + }, + { + title: 'Memory & ARC Cache Size', + type: 'TimeSeriesChart', + query: 'node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes', + unit: 'bytes', + }, + { + title: 'ZFS ARC Hit Ratio', + type: 'Gauge', + query: 'zfs_arc_hits / (zfs_arc_hits + zfs_arc_misses) * 100', + unit: '%', + value: 98.6, + }, + { + title: 'Storage Pool Throughput', + type: 'TimeSeriesChart', + query: 'rate(node_disk_read_bytes_total[1m]) + rate(node_disk_written_bytes_total[1m])', + unit: 'Bps', + }, + ], + }, + ]; + } +} diff --git a/src/services/rebuild.service.ts b/src/services/rebuild.service.ts new file mode 100644 index 0000000..919c0b5 --- /dev/null +++ b/src/services/rebuild.service.ts @@ -0,0 +1,71 @@ +import { spawn } from 'child_process'; +import { EventEmitter } from 'events'; +import { config } from '../config/index.js'; + +export interface RebuildEvent { + type: 'stdout' | 'stderr' | 'done' | 'error'; + data: string; +} + +export class RebuildService extends EventEmitter { + private isBuilding = false; + + getBuildingStatus(): boolean { + return this.isBuilding; + } + + async triggerSwitch(): Promise | EventEmitter> { + if (this.isBuilding) { + throw new Error('A rebuild is already in progress'); + } + + this.isBuilding = true; + const emitter = new EventEmitter(); + + if (!config.isLinux) { + // Mock build stream for development / testing environments + setTimeout(() => { + emitter.emit('data', '[dry-run] building NixOS configuration...\n'); + }, 200); + setTimeout(() => { + emitter.emit('data', '[dry-run] evaluating flake /etc/naxos/repo...\n'); + }, 500); + setTimeout(() => { + emitter.emit('data', '[switch] activating system configuration...\n'); + }, 900); + setTimeout(() => { + emitter.emit('data', '[canary] checking system services... SUCCESS\n'); + emitter.emit('data', 'NaxOS switch finished successfully.\n'); + this.isBuilding = false; + emitter.emit('end'); + }, 1400); + + return emitter; + } + + const proc = spawn('nixos-rebuild-safe', [], { + cwd: config.configRepoDir, + env: { ...process.env, PATH: `${process.env.PATH}:/run/current-system/sw/bin` }, + }); + + proc.stdout.on('data', (data) => { + emitter.emit('data', data.toString()); + }); + + proc.stderr.on('data', (data) => { + emitter.emit('data', data.toString()); + }); + + proc.on('close', (code) => { + this.isBuilding = false; + if (code === 0) { + emitter.emit('data', '\nRebuild and switch completed successfully.\n'); + } else { + emitter.emit('data', `\nRebuild failed with exit code ${code}\n`); + } + emitter.emit('end'); + }); + + return emitter; + } +} diff --git a/src/services/shares.service.ts b/src/services/shares.service.ts new file mode 100644 index 0000000..1fdfb20 --- /dev/null +++ b/src/services/shares.service.ts @@ -0,0 +1,62 @@ +import type { SmbShare, NfsExport } from '../types/index.js'; +import type { GitOpsService } from './gitops.service.js'; + +export class SharesService { + constructor(private gitopsService: GitOpsService) {} + + getSmbShares(): Record { + return this.gitopsService.getConfig().shares.samba.shares; + } + + async saveSmbShare(share: SmbShare): Promise { + const config = this.gitopsService.getConfig(); + config.shares.samba.shares[share.name] = share; + await this.gitopsService.updateConfig( + { shares: config.shares }, + `feat(shares): configured SMB share '${share.name}' at ${share.path}` + ); + return share; + } + + async deleteSmbShare(shareName: string): Promise<{ success: boolean; message: string }> { + const config = this.gitopsService.getConfig(); + if (!config.shares.samba.shares[shareName]) { + throw new Error(`SMB share '${shareName}' does not exist.`); + } + delete config.shares.samba.shares[shareName]; + await this.gitopsService.updateConfig( + { shares: config.shares }, + `feat(shares): removed SMB share '${shareName}'` + ); + return { success: true, message: `SMB share '${shareName}' deleted.` }; + } + + getNfsExports(): NfsExport[] { + return this.gitopsService.getConfig().shares.nfs.exports; + } + + async saveNfsExport(nfsExport: NfsExport): Promise { + const config = this.gitopsService.getConfig(); + const existingIndex = config.shares.nfs.exports.findIndex((e) => e.path === nfsExport.path); + if (existingIndex >= 0) { + config.shares.nfs.exports[existingIndex] = nfsExport; + } else { + config.shares.nfs.exports.push(nfsExport); + } + await this.gitopsService.updateConfig( + { shares: config.shares }, + `feat(shares): configured NFS export for '${nfsExport.path}'` + ); + return nfsExport; + } + + async deleteNfsExport(exportPath: string): Promise<{ success: boolean; message: string }> { + const config = this.gitopsService.getConfig(); + config.shares.nfs.exports = config.shares.nfs.exports.filter((e) => e.path !== exportPath); + await this.gitopsService.updateConfig( + { shares: config.shares }, + `feat(shares): removed NFS export for '${exportPath}'` + ); + return { success: true, message: `NFS export for '${exportPath}' deleted.` }; + } +} diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..6e6e40b --- /dev/null +++ b/src/services/users.service.ts @@ -0,0 +1,36 @@ +import type { SystemUser } from '../types/index.js'; +import type { GitOpsService } from './gitops.service.js'; + +export class UsersService { + constructor(private gitopsService: GitOpsService) {} + + getUsers(): Record { + return this.gitopsService.getConfig().users; + } + + async saveUser(user: SystemUser): Promise { + const config = this.gitopsService.getConfig(); + config.users[user.username] = user; + await this.gitopsService.updateConfig( + { users: config.users }, + `feat(users): updated account details for '${user.username}'` + ); + return user; + } + + async deleteUser(username: string): Promise<{ success: boolean; message: string }> { + const config = this.gitopsService.getConfig(); + if (username === 'admin') { + throw new Error('The primary appliance administrator account cannot be removed.'); + } + if (!config.users[username]) { + throw new Error(`User '${username}' does not exist.`); + } + delete config.users[username]; + await this.gitopsService.updateConfig( + { users: config.users }, + `feat(users): removed account '${username}'` + ); + return { success: true, message: `User '${username}' deleted.` }; + } +} diff --git a/src/services/zfs.service.ts b/src/services/zfs.service.ts new file mode 100644 index 0000000..68455e5 --- /dev/null +++ b/src/services/zfs.service.ts @@ -0,0 +1,495 @@ +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { config } from '../config/index.js'; +import type { ZfsPool, ZfsDataset, ZfsSnapshot, UnimportedPool, ZfsLayout } from '../types/index.js'; + +const execFileAsync = promisify(execFile); + +export class ZfsService { + private mockPools: ZfsPool[] = [ + { + name: 'tank', + size: '14.5T', + allocated: '8.2T', + free: '6.3T', + fragmentation: '14%', + capacityPercent: 56, + health: 'ONLINE', + altroot: '-', + scanStatus: 'scrub repaired 0B in 04:12:30 with 0 errors on Sun Sep 01 04:12:30 2026', + scrubProgress: 100, + vdevs: [ + { + name: 'mirror-0', + type: 'mirror', + health: 'ONLINE', + disks: [ + { + name: 'sda', + path: '/dev/disk/by-id/ata-WDC_WD100EFAX-68LHPN0_WD-WX11DC0E4J81', + size: '10T', + health: 'ONLINE', + readErrors: 0, + writeErrors: 0, + checksumErrors: 0, + }, + { + name: 'sdb', + path: '/dev/disk/by-id/ata-WDC_WD100EFAX-68LHPN0_WD-WX21DC0E9L32', + size: '10T', + health: 'ONLINE', + readErrors: 0, + writeErrors: 0, + checksumErrors: 0, + }, + ], + }, + ], + }, + ]; + + private mockDatasets: ZfsDataset[] = [ + { + name: 'tank', + pool: 'tank', + used: '8.2T', + available: '6.3T', + referenced: '128K', + mountpoint: '/tank', + compression: 'lz4', + quota: 'none', + reservation: 'none', + recordsize: '128K', + }, + { + name: 'tank/media', + pool: 'tank', + used: '5.1T', + available: '6.3T', + referenced: '5.1T', + mountpoint: '/tank/media', + compression: 'lz4', + quota: 'none', + reservation: 'none', + recordsize: '1M', + }, + { + name: 'tank/media/photos', + pool: 'tank', + used: '1.8T', + available: '6.3T', + referenced: '1.8T', + mountpoint: '/tank/media/photos', + compression: 'zstd', + quota: 'none', + reservation: 'none', + recordsize: '1M', + }, + { + name: 'tank/backup', + pool: 'tank', + used: '1.2T', + available: '6.3T', + referenced: '1.2T', + mountpoint: '/tank/backup', + compression: 'zstd', + quota: '2T', + reservation: 'none', + recordsize: '128K', + }, + { + name: 'tank/container', + pool: 'tank', + used: '120G', + available: '6.3T', + referenced: '120G', + mountpoint: '/var/lib/docker', + compression: 'lz4', + quota: '500G', + reservation: 'none', + recordsize: '128K', + }, + ]; + + private mockSnapshots: ZfsSnapshot[] = [ + { + name: 'tank/media/photos@daily-2026-09-02', + dataset: 'tank/media/photos', + snapshotTag: 'daily-2026-09-02', + creationTime: '2026-09-02T00:00:00Z', + usedBytes: '14.2G', + referencedBytes: '1.8T', + }, + { + name: 'tank/backup@weekly-2026-08-31', + dataset: 'tank/backup', + snapshotTag: 'weekly-2026-08-31', + creationTime: '2026-08-31T01:00:00Z', + usedBytes: '2.4G', + referencedBytes: '1.2T', + }, + ]; + + private mockUnimportedPools: UnimportedPool[] = [ + { + name: 'truenas_pool', + id: '12495810294819284', + state: 'ONLINE', + status: 'The pool was exported from another host and is ready for import.', + action: 'The pool can be imported using its name or numeric identifier.', + disks: ['/dev/sdc', '/dev/sdd'], + }, + ]; + + private async runCommand(cmd: string, args: string[]): Promise { + try { + const { stdout } = await execFileAsync(cmd, args); + return stdout.trim(); + } catch (err: any) { + throw new Error(`ZFS command error (${cmd} ${args.join(' ')}): ${err.stderr || err.message}`); + } + } + + async listPools(): Promise { + if (!config.isLinux) { + return this.mockPools; + } + try { + const stdout = await this.runCommand('zpool', ['list', '-H', '-p', '-o', 'name,size,alloc,free,frag,cap,health,altroot']); + if (!stdout) return []; + const lines = stdout.split('\n'); + return lines.map((line) => { + const [name, size, alloc, free, frag, cap, health, altroot] = line.split('\t'); + return { + name, + size, + allocated: alloc, + free, + fragmentation: `${frag}%`, + capacityPercent: parseInt(cap, 10) || 0, + health: (health as any) || 'ONLINE', + altroot, + vdevs: [], + }; + }); + } catch { + return this.mockPools; + } + } + + async getPool(poolName: string): Promise { + const pools = await this.listPools(); + return pools.find((p) => p.name === poolName) || null; + } + + async createPool(params: { + name: string; + layout: ZfsLayout; + devices: string[]; + ashift?: number; + }): Promise<{ success: boolean; message: string }> { + const ashift = params.ashift || 12; + if (!config.isLinux) { + const newPool: ZfsPool = { + name: params.name, + size: '19.0T', + allocated: '1.2M', + free: '19.0T', + fragmentation: '0%', + capacityPercent: 0, + health: 'ONLINE', + altroot: '-', + vdevs: [ + { + name: `${params.layout}-0`, + type: params.layout, + health: 'ONLINE', + disks: params.devices.map((dev) => ({ + name: dev.split('/').pop() || dev, + path: dev, + size: '10T', + health: 'ONLINE', + readErrors: 0, + writeErrors: 0, + checksumErrors: 0, + })), + }, + ], + }; + this.mockPools.push(newPool); + return { success: true, message: `Pool '${params.name}' created successfully.` }; + } + + const args = ['create', '-f', '-o', `ashift=${ashift}`, '-O', 'compression=lz4', '-O', 'xattr=sa', '-O', 'acltype=posixacl', params.name]; + if (params.layout !== 'stripe') { + args.push(params.layout); + } + args.push(...params.devices); + await this.runCommand('zpool', args); + return { success: true, message: `Pool '${params.name}' created successfully.` }; + } + + async scanUnimportedPools(): Promise { + if (!config.isLinux) { + return this.mockUnimportedPools; + } + try { + const stdout = await this.runCommand('zpool', ['import']); + const pools: UnimportedPool[] = []; + const lines = stdout.split('\n'); + let currentPool: Partial | null = null; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('pool:')) { + if (currentPool && currentPool.name) { + pools.push(currentPool as UnimportedPool); + } + currentPool = { + name: trimmed.replace('pool:', '').trim(), + disks: [], + state: 'UNKNOWN', + status: '', + id: '', + }; + } else if (currentPool) { + if (trimmed.startsWith('id:')) { + currentPool.id = trimmed.replace('id:', '').trim(); + } else if (trimmed.startsWith('state:')) { + currentPool.state = trimmed.replace('state:', '').trim(); + } else if (trimmed.startsWith('status:')) { + currentPool.status = trimmed.replace('status:', '').trim(); + } else if (trimmed.startsWith('action:')) { + currentPool.action = trimmed.replace('action:', '').trim(); + } + } + } + if (currentPool && currentPool.name) { + pools.push(currentPool as UnimportedPool); + } + return pools; + } catch { + return this.mockUnimportedPools; + } + } + + async importPool(params: { + poolName: string; + force?: boolean; + altroot?: string; + noMount?: boolean; + }): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + const found = this.mockUnimportedPools.find((p) => p.name === params.poolName); + if (found) { + this.mockUnimportedPools = this.mockUnimportedPools.filter((p) => p.name !== params.poolName); + this.mockPools.push({ + name: found.name, + size: '18.2T', + allocated: '4.1T', + free: '14.1T', + fragmentation: '3%', + capacityPercent: 22, + health: 'ONLINE', + altroot: params.altroot || '-', + vdevs: [], + }); + } + return { success: true, message: `Pool '${params.poolName}' imported safely without data loss.` }; + } + + const args = ['import']; + if (params.force) args.push('-f'); + if (params.noMount) args.push('-N'); + if (params.altroot) { + args.push('-R', params.altroot); + } + args.push(params.poolName); + await this.runCommand('zpool', args); + return { success: true, message: `Pool '${params.poolName}' imported safely without data loss.` }; + } + + async exportPool(poolName: string): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + this.mockPools = this.mockPools.filter((p) => p.name !== poolName); + return { success: true, message: `Pool '${poolName}' exported.` }; + } + await this.runCommand('zpool', ['export', poolName]); + return { success: true, message: `Pool '${poolName}' exported.` }; + } + + async scrub(poolName: string, action: 'start' | 'stop' | 'pause'): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + return { success: true, message: `Scrub ${action} requested for pool '${poolName}'.` }; + } + const flag = action === 'stop' ? '-s' : action === 'pause' ? '-p' : ''; + const args = ['scrub']; + if (flag) args.push(flag); + args.push(poolName); + await this.runCommand('zpool', args); + return { success: true, message: `Scrub ${action} initiated for '${poolName}'.` }; + } + + async trim(poolName: string): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + return { success: true, message: `TRIM initiated for pool '${poolName}'.` }; + } + await this.runCommand('zpool', ['trim', poolName]); + return { success: true, message: `TRIM operation started on '${poolName}'.` }; + } + + async listDatasets(poolName?: string): Promise { + if (!config.isLinux) { + if (poolName) { + return this.mockDatasets.filter((d) => d.pool === poolName); + } + return this.mockDatasets; + } + try { + const args = ['list', '-H', '-o', 'name,used,avail,refer,mountpoint,compression,quota,reservation,recordsize']; + if (poolName) args.push('-r', poolName); + const stdout = await this.runCommand('zfs', args); + if (!stdout) return []; + return stdout.split('\n').map((line) => { + const [name, used, available, referenced, mountpoint, compression, quota, reservation, recordsize] = line.split('\t'); + const pool = name.split('/')[0]; + return { + name, + pool, + used, + available, + referenced, + mountpoint, + compression, + quota, + reservation, + recordsize, + }; + }); + } catch { + return this.mockDatasets; + } + } + + async createDataset(params: { + name: string; + compression?: string; + recordsize?: string; + quota?: string; + mountpoint?: string; + }): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + const pool = params.name.split('/')[0]; + const newDs: ZfsDataset = { + name: params.name, + pool, + used: '0B', + available: '6.3T', + referenced: '0B', + mountpoint: params.mountpoint || `/${params.name}`, + compression: params.compression || 'lz4', + quota: params.quota || 'none', + reservation: 'none', + recordsize: params.recordsize || '128K', + }; + this.mockDatasets.push(newDs); + return { success: true, message: `Dataset '${params.name}' created.` }; + } + + const args = ['create']; + if (params.compression) args.push('-o', `compression=${params.compression}`); + if (params.recordsize) args.push('-o', `recordsize=${params.recordsize}`); + if (params.quota) args.push('-o', `quota=${params.quota}`); + if (params.mountpoint) args.push('-o', `mountpoint=${params.mountpoint}`); + args.push(params.name); + await this.runCommand('zfs', args); + return { success: true, message: `Dataset '${params.name}' created.` }; + } + + async destroyDataset(name: string, recursive = false): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + this.mockDatasets = this.mockDatasets.filter((d) => d.name !== name && (!recursive || !d.name.startsWith(`${name}/`))); + return { success: true, message: `Dataset '${name}' destroyed.` }; + } + const args = ['destroy']; + if (recursive) args.push('-r'); + args.push(name); + await this.runCommand('zfs', args); + return { success: true, message: `Dataset '${name}' destroyed.` }; + } + + async setDatasetProperty(name: string, property: string, value: string): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + const ds = this.mockDatasets.find((d) => d.name === name); + if (ds) { + (ds as any)[property] = value; + } + return { success: true, message: `Property '${property}' set to '${value}' for '${name}'.` }; + } + await this.runCommand('zfs', ['set', `${property}=${value}`, name]); + return { success: true, message: `Property '${property}' set to '${value}' for '${name}'.` }; + } + + async listSnapshots(datasetName?: string): Promise { + if (!config.isLinux) { + if (datasetName) { + return this.mockSnapshots.filter((s) => s.dataset === datasetName); + } + return this.mockSnapshots; + } + try { + const args = ['list', '-t', 'snapshot', '-H', '-o', 'name,creation,used,refer']; + if (datasetName) args.push('-r', datasetName); + const stdout = await this.runCommand('zfs', args); + if (!stdout) return []; + return stdout.split('\n').map((line) => { + const [fullName, creationTime, usedBytes, referencedBytes] = line.split('\t'); + const [dataset, snapshotTag] = fullName.split('@'); + return { + name: fullName, + dataset, + snapshotTag, + creationTime, + usedBytes, + referencedBytes, + }; + }); + } catch { + return this.mockSnapshots; + } + } + + async createSnapshot(datasetName: string, snapshotTag: string): Promise<{ success: boolean; message: string }> { + const fullName = `${datasetName}@${snapshotTag}`; + if (!config.isLinux) { + this.mockSnapshots.unshift({ + name: fullName, + dataset: datasetName, + snapshotTag, + creationTime: new Date().toISOString(), + usedBytes: '0B', + referencedBytes: '1.8T', + }); + return { success: true, message: `Snapshot '${fullName}' created.` }; + } + await this.runCommand('zfs', ['snapshot', fullName]); + return { success: true, message: `Snapshot '${fullName}' created.` }; + } + + async rollbackSnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + return { success: true, message: `Dataset rolled back to '${snapshotName}'.` }; + } + await this.runCommand('zfs', ['rollback', '-r', snapshotName]); + return { success: true, message: `Dataset rolled back to '${snapshotName}'.` }; + } + + async destroySnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> { + if (!config.isLinux) { + this.mockSnapshots = this.mockSnapshots.filter((s) => s.name !== snapshotName); + return { success: true, message: `Snapshot '${snapshotName}' destroyed.` }; + } + await this.runCommand('zfs', ['destroy', snapshotName]); + return { success: true, message: `Snapshot '${snapshotName}' destroyed.` }; + } +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..94b7b4f --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,200 @@ +export type ZfsLayout = 'stripe' | 'mirror' | 'raidz1' | 'raidz2' | 'raidz3'; + +export interface ZfsDisk { + name: string; + path: string; + size: string; + health: string; + readErrors: number; + writeErrors: number; + checksumErrors: number; +} + +export interface ZfsVdev { + name: string; + type: ZfsLayout | 'disk'; + health: string; + disks: ZfsDisk[]; +} + +export interface ZfsPool { + name: string; + size: string; + allocated: string; + free: string; + fragmentation: string; + capacityPercent: number; + health: 'ONLINE' | 'DEGRADED' | 'FAULTED' | 'OFFLINE' | 'UNAVAIL'; + altroot: string; + vdevs: ZfsVdev[]; + scanStatus?: string; + scrubProgress?: number; +} + +export interface ZfsDataset { + name: string; + pool: string; + used: string; + available: string; + referenced: string; + mountpoint: string; + compression: string; + quota: string; + reservation: string; + recordsize: string; +} + +export interface ZfsSnapshot { + name: string; + dataset: string; + snapshotTag: string; + creationTime: string; + usedBytes: string; + referencedBytes: string; +} + +export interface UnimportedPool { + name: string; + id: string; + state: string; + status: string; + action?: string; + disks: string[]; +} + +export interface SmbShare { + name: string; + path: string; + comment?: string; + readOnly: boolean; + browseable: boolean; + guestOk: boolean; + validUsers: string[]; + forceUser?: string; + forceGroup?: string; + timeMachine: boolean; + timeMachineMaxSize?: string; +} + +export interface NfsExportClient { + subnet: string; + options: string; +} + +export interface NfsExport { + path: string; + clients: NfsExportClient[]; +} + +export interface SystemUser { + username: string; + description?: string; + isAdmin: boolean; + sshKeys: string[]; + smbAccess: boolean; +} + +export type AppRuntime = 'systemd' | 'docker' | 'k3s'; + +export interface AppCatalogItem { + id: string; + name: string; + category: 'Media' | 'Storage' | 'Productivity' | 'Security' | 'Automation'; + description: string; + icon: string; + defaultPort: number; + supportedRuntimes: AppRuntime[]; + recommendedRuntime: AppRuntime; + envVariables?: Record; +} + +export interface InstalledApp { + id: string; + appId: string; + name: string; + runtime: AppRuntime; + status: 'running' | 'stopped' | 'errored' | 'starting'; + port: number; + version: string; + cpuUsagePercent: number; + memoryUsageBytes: number; + config: Record; +} + +export interface GitOpsStatus { + enabled: boolean; + remoteUrl?: string; + branch: string; + lastCommitSha: string; + lastCommitMessage: string; + lastSyncTime?: string; + pendingChanges: boolean; + isClean: boolean; +} + +export interface GitCommit { + hash: string; + author: string; + date: string; + message: string; +} + +export interface SystemStatus { + hostname: string; + uptimeSeconds: number; + cpuUsagePercent: number; + memoryTotalBytes: number; + memoryUsedBytes: number; + arcSizeBytes: number; + arcHitRatioPercent: number; + zfsPoolsCount: number; + activeSharesCount: number; + runningAppsCount: number; + osVersion: string; + nixosGeneration: number; +} + +export interface NaxosConfig { + core: { + hostname: string; + timezone: string; + }; + storage: { + arcMaxBytes: number; + autoScrub: boolean; + autoTrim: boolean; + pools: Record; + }>; + importExistingPools: string[]; + }; + shares: { + samba: { + enable: boolean; + workgroup: string; + shares: Record; + }; + nfs: { + enable: boolean; + exports: NfsExport[]; + }; + }; + appEngine: { + defaultRuntime: AppRuntime; + apps: Record; + }; + users: Record; + gitops: { + remoteUrl?: string; + branch: string; + autoPushOnCommit: boolean; + }; +} diff --git a/tests/apps.service.test.ts b/tests/apps.service.test.ts new file mode 100644 index 0000000..722293a --- /dev/null +++ b/tests/apps.service.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { GitOpsService } from '../src/services/gitops.service.js'; +import { AppsService } from '../src/services/apps.service.js'; +import os from 'os'; +import path from 'path'; +import fs from 'fs/promises'; + +describe('AppsService', () => { + let gitops: GitOpsService; + let apps: AppsService; + let testRepoDir: string; + + beforeEach(async () => { + testRepoDir = path.join(os.tmpdir(), `naxos-apps-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + gitops = new GitOpsService(testRepoDir); + await gitops.init(); + apps = new AppsService(gitops); + }); + + afterEach(async () => { + try { + await fs.rm(testRepoDir, { recursive: true, force: true }); + } catch {} + }); + + it('should return the curated app catalog', () => { + const catalog = apps.getCatalog(); + expect(catalog.length).toBeGreaterThanOrEqual(5); + const immich = catalog.find((c) => c.id === 'immich'); + expect(immich).toBeDefined(); + expect(immich?.recommendedRuntime).toBe('systemd'); + }); + + it('should install app into active workloads', async () => { + const installed = await apps.installApp({ appId: 'vaultwarden' }); + expect(installed.appId).toBe('vaultwarden'); + expect(installed.status).toBe('running'); + + const list = apps.getInstalledApps(); + expect(list.some((a) => a.appId === 'vaultwarden')).toBe(true); + }); +}); diff --git a/tests/gitops.service.test.ts b/tests/gitops.service.test.ts new file mode 100644 index 0000000..6c2b78d --- /dev/null +++ b/tests/gitops.service.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { GitOpsService } from '../src/services/gitops.service.js'; +import os from 'os'; +import path from 'path'; +import fs from 'fs/promises'; + +describe('GitOpsService', () => { + let gitops: GitOpsService; + let testRepoDir: string; + + beforeEach(async () => { + testRepoDir = path.join(os.tmpdir(), `naxos-gitops-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + gitops = new GitOpsService(testRepoDir); + await gitops.init(); + }); + + afterEach(async () => { + try { + await fs.rm(testRepoDir, { recursive: true, force: true }); + } catch {} + }); + + it('should initialize and produce valid GitOps status', async () => { + const status = await gitops.getStatus(); + expect(status.enabled).toBe(true); + expect(status.branch).toBe('main'); + expect(status.lastCommitSha).toBeDefined(); + }); + + it('should translate declarative configuration into valid NixOS module code', async () => { + const nixCode = await gitops.renderNixOSModule(); + expect(nixCode).toContain('services.naxos.storage'); + expect(nixCode).toContain('services.naxos.shares.samba'); + expect(nixCode).toContain('importExistingPools'); + expect(nixCode).toContain('tank'); + }); + + it('should create commits on state changes', async () => { + const res = await gitops.updateConfig( + { + core: { hostname: 'naxos-test', timezone: 'UTC' }, + }, + 'feat(test): updated hostname' + ); + expect(res.commitSha).toBeDefined(); + expect(res.commitSha.length).toBeGreaterThan(10); + + const commits = await gitops.getCommits(5); + expect(commits[0].message).toBe('feat(test): updated hostname'); + }); +}); diff --git a/tests/zfs.service.test.ts b/tests/zfs.service.test.ts new file mode 100644 index 0000000..c405d90 --- /dev/null +++ b/tests/zfs.service.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { ZfsService } from '../src/services/zfs.service.js'; + +describe('ZfsService', () => { + const zfs = new ZfsService(); + + it('should list configured ZFS pools with health status', async () => { + const pools = await zfs.listPools(); + expect(pools).toBeDefined(); + expect(pools.length).toBeGreaterThan(0); + const tank = pools.find((p) => p.name === 'tank'); + expect(tank).toBeDefined(); + expect(tank?.health).toBe('ONLINE'); + expect(tank?.capacityPercent).toBeGreaterThanOrEqual(0); + }); + + it('should list datasets with proper mountpoint and compression', async () => { + const datasets = await zfs.listDatasets('tank'); + expect(datasets.length).toBeGreaterThan(0); + const photos = datasets.find((d) => d.name === 'tank/media/photos'); + expect(photos).toBeDefined(); + expect(photos?.mountpoint).toBe('/tank/media/photos'); + expect(photos?.compression).toBe('zstd'); + }); + + it('should scan for unimported foreign pools without data loss', async () => { + const unimported = await zfs.scanUnimportedPools(); + expect(unimported).toBeDefined(); + expect(Array.isArray(unimported)).toBe(true); + }); + + it('should safely import a foreign pool', async () => { + const res = await zfs.importPool({ poolName: 'truenas_pool', force: true, noMount: true }); + expect(res.success).toBe(true); + const pools = await zfs.listPools(); + const imported = pools.find((p) => p.name === 'truenas_pool'); + expect(imported).toBeDefined(); + }); + + it('should create and rollback snapshots', async () => { + const snapTag = `test-${Date.now()}`; + const createRes = await zfs.createSnapshot('tank/media/photos', snapTag); + expect(createRes.success).toBe(true); + + const snapshots = await zfs.listSnapshots('tank/media/photos'); + const created = snapshots.find((s) => s.snapshotTag === snapTag); + expect(created).toBeDefined(); + + const rollbackRes = await zfs.rollbackSnapshot(`tank/media/photos@${snapTag}`); + expect(rollbackRes.success).toBe(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9715bc8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +}