From 177e993736f6b91063cd7fcda9dfb9208ba83d0b Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 17 Mar 2026 02:55:52 +0000 Subject: [PATCH] feat: TypeScript bastion rewrite (initial scaffold) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full rewrite of the bash bastion.sh into a TypeScript application: - Fastify HTTP server with typed routes (dispatch, kickstart, API) - Commander CLI (serve, install, list, reprovision) - Kickstart templates as TypeScript template literals (no more heredoc hell) - dnsmasq management via execa subprocess - Merged machine list view (hardware + install info in one table) - Containerized via podman-compose (Dockerfile + docker-compose.yml) - All partition logic preserved (LVM, reprovision detection, role-based) Not yet tested end-to-end — needs VM validation before replacing bash version. Co-Authored-By: Claude Opus 4.6 (1M context) --- bastion/.gitignore | 3 + bastion/package.json | 39 + bastion/pnpm-lock.yaml | 1954 +++++++++++++++++ bastion/src/cli/commands/install.ts | 44 + bastion/src/cli/commands/list.ts | 101 + bastion/src/cli/commands/reprovision.ts | 86 + bastion/src/cli/commands/serve.ts | 40 + bastion/src/cli/index.ts | 28 + bastion/src/server/config.ts | 67 + bastion/src/server/main.ts | 177 ++ bastion/src/server/routes/api.ts | 164 ++ bastion/src/server/routes/dispatch.ts | 64 + bastion/src/server/routes/kickstart.ts | 34 + bastion/src/server/server.ts | 61 + bastion/src/server/services/dnsmasq.ts | 70 + .../server/services/kickstart-generator.ts | 44 + bastion/src/server/services/logger.ts | 17 + bastion/src/server/services/network.ts | 158 ++ bastion/src/server/services/state.ts | 92 + bastion/src/templates/boot.ipxe.ts | 93 + bastion/src/templates/discover.ks.ts | 118 + bastion/src/templates/dnsmasq.conf.ts | 88 + bastion/src/templates/install.ks.ts | 365 +++ bastion/stack/.env.example | 33 + bastion/stack/Dockerfile | 37 + bastion/stack/docker-compose.yml | 21 + bastion/tsconfig.json | 27 + 27 files changed, 4025 insertions(+) create mode 100644 bastion/.gitignore create mode 100644 bastion/package.json create mode 100644 bastion/pnpm-lock.yaml create mode 100644 bastion/src/cli/commands/install.ts create mode 100644 bastion/src/cli/commands/list.ts create mode 100644 bastion/src/cli/commands/reprovision.ts create mode 100644 bastion/src/cli/commands/serve.ts create mode 100644 bastion/src/cli/index.ts create mode 100644 bastion/src/server/config.ts create mode 100644 bastion/src/server/main.ts create mode 100644 bastion/src/server/routes/api.ts create mode 100644 bastion/src/server/routes/dispatch.ts create mode 100644 bastion/src/server/routes/kickstart.ts create mode 100644 bastion/src/server/server.ts create mode 100644 bastion/src/server/services/dnsmasq.ts create mode 100644 bastion/src/server/services/kickstart-generator.ts create mode 100644 bastion/src/server/services/logger.ts create mode 100644 bastion/src/server/services/network.ts create mode 100644 bastion/src/server/services/state.ts create mode 100644 bastion/src/templates/boot.ipxe.ts create mode 100644 bastion/src/templates/discover.ks.ts create mode 100644 bastion/src/templates/dnsmasq.conf.ts create mode 100644 bastion/src/templates/install.ks.ts create mode 100644 bastion/stack/.env.example create mode 100644 bastion/stack/Dockerfile create mode 100644 bastion/stack/docker-compose.yml create mode 100644 bastion/tsconfig.json diff --git a/bastion/.gitignore b/bastion/.gitignore new file mode 100644 index 0000000..f4e2c6d --- /dev/null +++ b/bastion/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/bastion/package.json b/bastion/package.json new file mode 100644 index 0000000..29fb506 --- /dev/null +++ b/bastion/package.json @@ -0,0 +1,39 @@ +{ + "name": "lab-bastion", + "version": "0.1.0", + "private": true, + "description": "PXE bastion server for discover-first bare-metal provisioning", + "type": "module", + "bin": { + "bastion": "./dist/cli/index.js" + }, + "main": "./dist/server/main.js", + "scripts": { + "build": "tsc", + "dev": "tsx src/cli/index.ts", + "start": "node dist/cli/index.js", + "test": "vitest", + "test:run": "vitest run", + "lint": "tsc --noEmit", + "clean": "rimraf dist" + }, + "engines": { + "node": ">=20.0.0", + "pnpm": ">=9.0.0" + }, + "packageManager": "pnpm@9.15.0", + "dependencies": { + "@fastify/static": "^8.0.0", + "commander": "^13.0.0", + "execa": "^9.5.0", + "fastify": "^5.0.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "rimraf": "^6.0.0", + "tsx": "^4.21.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } +} diff --git a/bastion/pnpm-lock.yaml b/bastion/pnpm-lock.yaml new file mode 100644 index 0000000..a437756 --- /dev/null +++ b/bastion/pnpm-lock.yaml @@ -0,0 +1,1954 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fastify/static': + specifier: ^8.0.0 + version: 8.3.0 + commander: + specifier: ^13.0.0 + version: 13.1.0 + execa: + specifier: ^9.5.0 + version: 9.6.1 + fastify: + specifier: ^5.0.0 + version: 5.8.2 + winston: + specifier: ^3.17.0 + version: 3.19.0 + devDependencies: + '@types/node': + specifier: ^22.10.0 + version: 22.19.15 + rimraf: + specifier: ^6.0.0 + version: 6.1.3 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@22.19.15)(tsx@4.21.0) + +packages: + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@fastify/send@4.1.0': + resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} + + '@fastify/static@8.3.0': + resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify@5.8.2: + resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + +snapshots: + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@fastify/accept-negotiator@2.0.1': {} + + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + + '@fastify/send@4.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.1 + mime: 3.0.0 + + '@fastify/static@8.3.0': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 0.5.4 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 11.1.0 + + '@isaacs/cliui@9.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@lukeed/ms@2.0.2': {} + + '@pinojs/redact@0.4.0': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@types/triple-beam@1.3.5': {} + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.15)(tsx@4.21.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + abstract-logging@2.0.1: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + assertion-error@2.0.1: {} + + async@3.2.6: {} + + atomic-sleep@1.0.0: {} + + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + + balanced-match@4.0.4: {} + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + commander@13.1.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + enabled@2.0.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + expect-type@1.3.0: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.0: {} + + fastify-plugin@5.1.0: {} + + fastify@5.8.2: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fecha@4.2.3: {} + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 + + fn.name@1.1.0: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.4 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + human-signals@8.0.1: {} + + inherits@2.0.4: {} + + ipaddr.js@2.3.0: {} + + is-plain-obj@4.1.0: {} + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + js-tokens@9.0.1: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-traverse@1.0.0: {} + + kuler@2.0.0: {} + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + loupe@3.2.1: {} + + lru-cache@11.2.7: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mime@3.0.0: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + on-exit-leak-free@2.1.2: {} + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + package-json-from-dist@1.0.1: {} + + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + quick-format-unescaped@4.0.4: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + real-require@0.2.0: {} + + require-from-string@2.0.2: {} + + resolve-pkg-maps@1.0.0: {} + + ret@0.5.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + secure-json-parse@4.1.0: {} + + semver@7.7.4: {} + + set-cookie-parser@2.7.2: {} + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + stack-trace@0.0.10: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-final-newline@4.0.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + text-hex@1.0.0: {} + + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + toad-cache@3.7.0: {} + + toidentifier@1.0.1: {} + + triple-beam@1.4.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unicorn-magic@0.3.0: {} + + util-deprecate@1.0.2: {} + + vite-node@3.2.4(@types/node@22.19.15)(tsx@4.21.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@22.19.15)(tsx@4.21.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@22.19.15)(tsx@4.21.0): + dependencies: + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@3.2.4(@types/node@22.19.15)(tsx@4.21.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(tsx@4.21.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@22.19.15)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@22.19.15)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + yoctocolors@2.1.2: {} diff --git a/bastion/src/cli/commands/install.ts b/bastion/src/cli/commands/install.ts new file mode 100644 index 0000000..6b6ef53 --- /dev/null +++ b/bastion/src/cli/commands/install.ts @@ -0,0 +1,44 @@ +// CLI command: install +// Queue a discovered machine for Fedora installation. + +import type { Command } from "commander"; + +export function registerInstallCommand(program: Command): void { + program + .command("install ") + .description("Queue a discovered machine for Fedora installation") + .option("--role ", "Machine role: worker or infra", "worker") + .option("--disk ", "Target disk device (auto-detect if omitted)") + .option("--port ", "Bastion HTTP port", "8080") + .action(async (mac: string, hostname: string, opts: { + role: string; + disk?: string; + port: string; + }) => { + const port = parseInt(opts.port, 10); + const payload: Record = { + mac, + hostname, + role: opts.role, + }; + if (opts.disk) { + payload["disk"] = opts.disk; + } + + try { + const response = await fetch(`http://localhost:${port}/api/install`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const result = await response.json() as Record; + console.log(JSON.stringify(result, null, 2)); + console.log(""); + console.log("Power on the machine to start Fedora installation."); + } catch { + console.error(`Cannot reach bastion at localhost:${port}. Is it running?`); + process.exit(1); + } + }); +} diff --git a/bastion/src/cli/commands/list.ts b/bastion/src/cli/commands/list.ts new file mode 100644 index 0000000..9fdb7c2 --- /dev/null +++ b/bastion/src/cli/commands/list.ts @@ -0,0 +1,101 @@ +// CLI command: list +// Merged view of all known machines with hardware + install info. + +import type { Command } from "commander"; +import type { BastionState } from "../../server/services/state.js"; + +const BOLD = "\x1b[1m"; +const GREEN = "\x1b[0;32m"; +const YELLOW = "\x1b[1;33m"; +const CYAN = "\x1b[0;36m"; +const RESET = "\x1b[0m"; + +function statusColor(status: string): string { + switch (status) { + case "installed": return GREEN; + case "queued": + case "installing": return YELLOW; + case "discovered": return CYAN; + default: return RESET; + } +} + +export function registerListCommand(program: Command): void { + program + .command("list") + .description("List all known machines") + .option("--port ", "Bastion HTTP port", "8080") + .action(async (opts: { port: string }) => { + const port = parseInt(opts.port, 10); + + let state: BastionState; + try { + const response = await fetch(`http://localhost:${port}/api/machines`); + state = (await response.json()) as BastionState; + } catch { + console.error(`Cannot reach bastion at localhost:${port}. Is it running?`); + process.exit(1); + } + + // Collect all known MACs + const allMacs = new Set([ + ...Object.keys(state.discovered), + ...Object.keys(state.install_queue), + ...Object.keys(state.installed), + ]); + + console.log(""); + if (allMacs.size === 0) { + console.log(" No machines known. PXE boot a machine to discover it."); + console.log(""); + return; + } + + console.log( + `${BOLD} ${"MAC".padEnd(20)} ${"HOSTNAME".padEnd(24)} ${"STATUS".padEnd(12)} ${"ROLE".padEnd(8)} ${"IP".padEnd(16)} ${"CPU".padEnd(24)} ${"CORES".padEnd(6)} ${"RAM".padEnd(6)} PRODUCT${RESET}`, + ); + + for (const mac of allMacs) { + const hw = state.discovered[mac]; + const queued = state.install_queue[mac]; + const inst = state.installed[mac]; + + // Determine status + let status = "discovered"; + if (queued) { + status = queued.progress && queued.progress !== "waiting" + ? "installing" + : "queued"; + } + if (inst) status = "installed"; + + const hostname = inst?.hostname ?? queued?.hostname ?? "-"; + const role = inst?.role ?? queued?.role ?? "-"; + const ip = inst?.ip ?? "-"; + const cpu = hw?.cpu_model ?? "-"; + const cores = hw?.cpu_cores != null ? String(hw.cpu_cores) : "-"; + const ram = hw?.memory_gb != null ? `${hw.memory_gb}GB` : "-"; + const product = hw?.product ?? "-"; + + const color = statusColor(status); + + console.log( + ` ${mac.padEnd(20)} ${hostname.padEnd(24)} ${color}${status.padEnd(12)}${RESET} ${role.padEnd(8)} ${ip.padEnd(16)} ${cpu.substring(0, 23).padEnd(24)} ${cores.padEnd(6)} ${ram.padEnd(6)} ${product}`, + ); + } + + // Show install queue details if any + const queueEntries = Object.entries(state.install_queue); + if (queueEntries.length > 0) { + console.log(""); + console.log(`${BOLD}PENDING${RESET}`); + for (const [mac, cfg] of queueEntries) { + const progress = cfg.progress ?? "waiting"; + const detail = cfg.progress_detail ?? ""; + console.log(` ${mac} ${progress}${detail ? ` - ${detail}` : ""}`); + } + } + + console.log(""); + }); +} diff --git a/bastion/src/cli/commands/reprovision.ts b/bastion/src/cli/commands/reprovision.ts new file mode 100644 index 0000000..eb33fec --- /dev/null +++ b/bastion/src/cli/commands/reprovision.ts @@ -0,0 +1,86 @@ +// CLI command: reprovision +// Queue a machine for reinstall and attempt SSH reboot into PXE. + +import { execSync } from "node:child_process"; +import type { Command } from "commander"; +import type { BastionState } from "../../server/services/state.js"; + +export function registerReprovisionCommand(program: Command): void { + program + .command("reprovision ") + .description("Queue install + SSH reboot into PXE for reprovision") + .option("--role ", "Machine role: worker or infra", "worker") + .option("--disk ", "Target disk device (auto-detect if omitted)") + .option("--port ", "Bastion HTTP port", "8080") + .action(async (mac: string, hostname: string, opts: { + role: string; + disk?: string; + port: string; + }) => { + const port = parseInt(opts.port, 10); + + // Queue the install + const payload: Record = { + mac, + hostname, + role: opts.role, + }; + if (opts.disk) { + payload["disk"] = opts.disk; + } + + let state: BastionState; + try { + const installResponse = await fetch(`http://localhost:${port}/api/install`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + const result = await installResponse.json() as Record; + console.log(JSON.stringify(result, null, 2)); + } catch { + console.error(`Cannot reach bastion at localhost:${port}. Is it running?`); + process.exit(1); + } + + // Try to find IP from installed state and SSH in to trigger PXE reboot + try { + const machinesResponse = await fetch(`http://localhost:${port}/api/machines`); + state = (await machinesResponse.json()) as BastionState; + } catch { + console.log(""); + console.log("Could not fetch machine state. Reboot the machine manually into PXE."); + return; + } + + const installedEntry = state.installed[mac.toLowerCase().replace(/-/g, ":")]; + const ip = installedEntry?.ip ?? ""; + const adminUser = process.env["SUDO_USER"] ?? process.env["USER"] ?? ""; + const effectiveUser = adminUser === "root" ? "" : adminUser; + + if (ip && effectiveUser) { + console.log(""); + console.log(`Attempting SSH reboot into PXE (${effectiveUser}@${ip})...`); + + try { + const sshCmd = [ + "ssh", + "-o", "StrictHostKeyChecking=no", + "-o", "ConnectTimeout=5", + `${effectiveUser}@${ip}`, + 'sudo efibootmgr 2>/dev/null; PXE_ENTRY=$(sudo efibootmgr | grep -iE "pxe|network|ipv4" | head -1 | grep -oP "Boot\\K[0-9A-F]+"); if [ -n "$PXE_ENTRY" ]; then sudo efibootmgr --bootnext "$PXE_ENTRY" && echo "PXE set as next boot" && sudo reboot; else echo "No PXE boot entry found, rebooting anyway..." && sudo reboot; fi', + ].join(" "); + + execSync(sshCmd, { stdio: "inherit" }); + console.log(""); + console.log("Machine is rebooting into PXE. Install will start automatically."); + } catch { + console.log(""); + console.log("SSH failed. Reboot the machine manually into PXE (e.g. via IPMI/KVM)."); + } + } else { + console.log(""); + console.log("No IP known for this machine. Reboot it manually into PXE."); + } + }); +} diff --git a/bastion/src/cli/commands/serve.ts b/bastion/src/cli/commands/serve.ts new file mode 100644 index 0000000..ab3bb56 --- /dev/null +++ b/bastion/src/cli/commands/serve.ts @@ -0,0 +1,40 @@ +// CLI command: serve +// Start the bastion server (HTTP + dnsmasq). + +import type { Command } from "commander"; +import { startBastion } from "../../server/main.js"; + +export function registerServeCommand(program: Command): void { + program + .command("serve") + .description("Start the bastion server (HTTP + dnsmasq PXE)") + .option("--port ", "HTTP port", "8080") + .option("--dir ", "Bastion data directory", "/tmp/lab-bastion") + .option("--domain ", "Internal domain for hostnames", "ad.itaz.eu") + .option("--dhcp-mode ", "DHCP mode: proxy or full", "proxy") + .option("--fedora ", "Fedora version", "43") + .option("--arch ", "Architecture", "x86_64") + .option("--timezone ", "Timezone", "Europe/London") + .option("--locale ", "Locale", "en_GB.UTF-8") + .action(async (opts: { + port: string; + dir: string; + domain: string; + dhcpMode: string; + fedora: string; + arch: string; + timezone: string; + locale: string; + }) => { + await startBastion({ + httpPort: parseInt(opts.port, 10), + bastionDir: opts.dir, + domain: opts.domain, + dhcpMode: opts.dhcpMode as "proxy" | "full", + fedoraVersion: opts.fedora, + arch: opts.arch, + timezone: opts.timezone, + locale: opts.locale, + }); + }); +} diff --git a/bastion/src/cli/index.ts b/bastion/src/cli/index.ts new file mode 100644 index 0000000..408aa77 --- /dev/null +++ b/bastion/src/cli/index.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +// CLI entry point for lab-bastion. +// Commands: serve, install, list, reprovision + +import { Command } from "commander"; +import { registerServeCommand } from "./commands/serve.js"; +import { registerInstallCommand } from "./commands/install.js"; +import { registerListCommand } from "./commands/list.js"; +import { registerReprovisionCommand } from "./commands/reprovision.js"; + +const program = new Command(); + +program + .name("bastion") + .description("Lab PXE Bastion -- discover-first bare-metal provisioning") + .version("0.1.0"); + +registerServeCommand(program); +registerInstallCommand(program); +registerListCommand(program); +registerReprovisionCommand(program); + +// Default to serve if no command given +program.action(() => { + program.commands.find((c) => c.name() === "serve")?.parseAsync(process.argv); +}); + +program.parse(); diff --git a/bastion/src/server/config.ts b/bastion/src/server/config.ts new file mode 100644 index 0000000..068bc9f --- /dev/null +++ b/bastion/src/server/config.ts @@ -0,0 +1,67 @@ +// Configuration from environment variables with sensible defaults. + +export interface BastionConfig { + fedoraVersion: string; + arch: string; + httpPort: number; + timezone: string; + locale: string; + bastionDir: string; + domain: string; + dhcpMode: "proxy" | "full"; + dhcpRangeStart: string; + dhcpRangeEnd: string; + // Derived at runtime + iface: string; + serverIp: string; + network: string; + gateway: string; + sshKeys: string[]; + adminUser: string; + fedoraMirror: string; + tftpDir: string; + httpDir: string; + stateFile: string; +} + +export function loadConfig(overrides: Partial = {}): BastionConfig { + const fedoraVersion = overrides.fedoraVersion ?? process.env["FEDORA_VERSION"] ?? "43"; + const arch = overrides.arch ?? process.env["ARCH"] ?? "x86_64"; + const httpPort = overrides.httpPort ?? parseInt(process.env["HTTP_PORT"] ?? "8080", 10); + const timezone = overrides.timezone ?? process.env["TIMEZONE"] ?? "Europe/London"; + const locale = overrides.locale ?? process.env["LOCALE"] ?? "en_GB.UTF-8"; + const bastionDir = overrides.bastionDir ?? process.env["BASTION_DIR"] ?? "/tmp/lab-bastion"; + const domain = overrides.domain ?? process.env["DOMAIN"] ?? "ad.itaz.eu"; + const dhcpMode = (overrides.dhcpMode ?? process.env["DHCP_MODE"] ?? "proxy") as "proxy" | "full"; + const dhcpRangeStart = overrides.dhcpRangeStart ?? process.env["DHCP_RANGE_START"] ?? ""; + const dhcpRangeEnd = overrides.dhcpRangeEnd ?? process.env["DHCP_RANGE_END"] ?? ""; + + const fedoraMirror = `https://download.fedoraproject.org/pub/fedora/linux/releases/${fedoraVersion}/Everything/${arch}/os`; + const tftpDir = `${bastionDir}/tftp`; + const httpDir = `${bastionDir}/http`; + const stateFile = `${bastionDir}/state.json`; + + return { + fedoraVersion, + arch, + httpPort, + timezone, + locale, + bastionDir, + domain, + dhcpMode, + dhcpRangeStart, + dhcpRangeEnd, + // These are populated at runtime by the network service + iface: overrides.iface ?? "", + serverIp: overrides.serverIp ?? "", + network: overrides.network ?? "", + gateway: overrides.gateway ?? "", + sshKeys: overrides.sshKeys ?? [], + adminUser: overrides.adminUser ?? "", + fedoraMirror, + tftpDir, + httpDir, + stateFile, + }; +} diff --git a/bastion/src/server/main.ts b/bastion/src/server/main.ts new file mode 100644 index 0000000..b8183a5 --- /dev/null +++ b/bastion/src/server/main.ts @@ -0,0 +1,177 @@ +// Entry point for the bastion server. +// Starts the Fastify HTTP server, dnsmasq, and handles graceful shutdown. + +import { mkdirSync, writeFileSync, existsSync, copyFileSync, symlinkSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { loadConfig, type BastionConfig } from "./config.js"; +import { populateNetworkConfig } from "./services/network.js"; +import { createApp } from "./server.js"; +import { startDnsmasq, stopDnsmasq, generateDnsmasqConf } from "./services/dnsmasq.js"; +import { generateDiscoverKickstart } from "./services/kickstart-generator.js"; +import { renderBootIpxe } from "../templates/boot.ipxe.js"; +import { logger } from "./services/logger.js"; + +function copyIfMissing(src: string, dest: string, label: string): void { + if (existsSync(dest)) { + logger.info(` ${label} -- cached`); + return; + } + if (!existsSync(src)) { + throw new Error(`${label}: source not found at ${src}`); + } + copyFileSync(src, dest); + logger.info(` ${label} -- copied from ${src}`); +} + +function download(url: string, dest: string, label: string): void { + if (existsSync(dest)) { + logger.info(` ${label} -- cached`); + return; + } + logger.info(` ${label} -- downloading...`); + try { + execSync(`curl -# -L -f -o "${dest}" "${url}"`, { stdio: "inherit" }); + } catch { + throw new Error(`Failed to download ${label} from ${url}`); + } +} + +function symlinkSafe(target: string, linkPath: string): void { + try { + symlinkSync(target, linkPath); + } catch { + // Link may already exist + } +} + +export async function startBastion(overrides: Partial = {}): Promise { + // Load and populate config + let config = loadConfig(overrides); + config = populateNetworkConfig(config); + + // Prepare directories + mkdirSync(config.tftpDir, { recursive: true }); + mkdirSync(config.httpDir, { recursive: true }); + + // Prepare boot artifacts + logger.info(`Preparing boot artifacts (Fedora ${config.fedoraVersion} ${config.arch})...`); + + copyIfMissing( + "/usr/share/ipxe/undionly.kpxe", + `${config.tftpDir}/undionly.kpxe`, + "iPXE BIOS", + ); + copyIfMissing( + "/usr/share/ipxe/ipxe-snponly-x86_64.efi", + `${config.tftpDir}/ipxe.efi`, + "iPXE UEFI x86_64", + ); + try { + copyIfMissing( + "/usr/share/ipxe/arm64-efi/snponly.efi", + `${config.tftpDir}/ipxe-arm64.efi`, + "iPXE UEFI arm64", + ); + } catch { + logger.warn("arm64 iPXE not available -- skipping"); + } + + download( + `${config.fedoraMirror}/images/pxeboot/vmlinuz`, + `${config.httpDir}/vmlinuz`, + "Fedora kernel", + ); + download( + `${config.fedoraMirror}/images/pxeboot/initrd.img`, + `${config.httpDir}/initrd.img`, + "Fedora initrd", + ); + + // Symlink iPXE binaries into HTTP dir for UEFI HTTP Boot + for (const name of ["ipxe.efi", "ipxe-arm64.efi"]) { + const src = `${config.tftpDir}/${name}`; + const dest = `${config.httpDir}/${name}`; + if (existsSync(src)) { + symlinkSafe(src, dest); + } + } + + // Write discovery kickstart + const discoverKs = generateDiscoverKickstart(config); + writeFileSync(`${config.httpDir}/discover.ks`, discoverKs); + + // Write iPXE boot script + const bootIpxe = renderBootIpxe({ + serverIp: config.serverIp, + httpPort: config.httpPort, + }); + writeFileSync(`${config.httpDir}/boot.ipxe`, bootIpxe); + + // Generate dnsmasq config + generateDnsmasqConf(config); + + // Start HTTP server + const { app } = createApp(config); + await app.listen({ port: config.httpPort, host: "0.0.0.0" }); + logger.info(`HTTP server listening on :${config.httpPort}`); + + // Start dnsmasq + const dnsmasqProc = await startDnsmasq(config); + + // Print banner + printBanner(config); + + // Graceful shutdown + const shutdown = async () => { + logger.info("Shutting down..."); + stopDnsmasq(); + await app.close(); + logger.info(`State preserved in ${config.stateFile}`); + process.exit(0); + }; + + process.on("SIGINT", () => void shutdown()); + process.on("SIGTERM", () => void shutdown()); + + // Wait for dnsmasq to exit + try { + await dnsmasqProc; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (!message.includes("was killed")) { + logger.error(`dnsmasq exited unexpectedly: ${message}`); + logger.error("Check if another DHCP/TFTP service is running."); + process.exit(1); + } + } +} + +function printBanner(config: BastionConfig): void { + const dhcpInfo = config.dhcpMode === "full" + ? `full (${config.dhcpRangeStart}-${config.dhcpRangeEnd})` + : "proxy (alongside existing DHCP)"; + + console.log(""); + console.log("\x1b[36m\x1b[1m" + "=".repeat(60) + "\x1b[0m"); + console.log("\x1b[36m\x1b[1m Lab PXE Bastion -- Discovery Mode\x1b[0m"); + console.log("\x1b[36m\x1b[1m" + "=".repeat(60) + "\x1b[0m"); + console.log(""); + console.log(` Network: \x1b[1m${config.network}/24\x1b[0m via \x1b[1m${config.iface}\x1b[0m`); + console.log(` DHCP: \x1b[1m${dhcpInfo}\x1b[0m`); + console.log(` HTTP: \x1b[1mhttp://${config.serverIp}:${config.httpPort}/\x1b[0m`); + console.log(` OS: \x1b[1mFedora ${config.fedoraVersion} (${config.arch})\x1b[0m`); + console.log(` Domain: \x1b[1m${config.domain}\x1b[0m`); + console.log(` State: \x1b[1m${config.stateFile}\x1b[0m`); + console.log(""); + console.log(" \x1b[33mPXE boot any machine on this network.\x1b[0m"); + console.log(" \x1b[33mIt will be inventoried and rebooted automatically.\x1b[0m"); + console.log(""); + console.log(" Commands (from another terminal):"); + console.log(" \x1b[1mbastion list\x1b[0m -- show machines"); + console.log(" \x1b[1mbastion install \x1b[0m -- queue install"); + console.log(""); + console.log(" Press \x1b[1mCtrl-C\x1b[0m to stop."); + console.log(""); + console.log("\x1b[36m---- Waiting for PXE boot requests... ----\x1b[0m"); + console.log(""); +} diff --git a/bastion/src/server/routes/api.ts b/bastion/src/server/routes/api.ts new file mode 100644 index 0000000..b37ddb0 --- /dev/null +++ b/bastion/src/server/routes/api.ts @@ -0,0 +1,164 @@ +// REST API routes for machine management. +// /api/machines - list all machines by state +// /api/install - queue a machine for install +// /api/progress - receive install progress callbacks from kickstart +// /api/discover - receive hardware discovery reports from PXE-booted machines + +import type { FastifyInstance } from "fastify"; +import type { StateManager, HardwareInfo, InstalledInfo } from "../services/state.js"; +import { logger } from "../services/logger.js"; + +export function registerApiRoutes( + app: FastifyInstance, + state: StateManager, +): void { + // List all machines + app.get("/api/machines", async (_request, reply) => { + return reply.send(state.load()); + }); + + // Queue a machine for install + app.post<{ + Body: { + mac?: string; + hostname?: string; + disk?: string; + role?: string; + }; + }>("/api/install", async (request, reply) => { + const { mac: rawMac, hostname, disk, role } = request.body ?? {}; + const mac = (rawMac ?? "").toLowerCase().replace(/-/g, ":"); + + if (!mac) { + return reply.status(400).send({ error: "mac is required" }); + } + + const validRole = role ?? "worker"; + if (validRole !== "worker" && validRole !== "infra") { + return reply.status(400).send({ error: "role must be 'worker' or 'infra'" }); + } + + state.update((s) => { + s.install_queue[mac] = { + hostname: hostname ?? "lab-node", + disk: disk ?? "", + role: validRole as "worker" | "infra", + queued_at: new Date().toISOString(), + }; + }); + + logger.info(`INSTALL QUEUED: ${mac} -> hostname=${hostname ?? "lab-node"} role=${validRole}`); + + return reply.send({ + status: "queued", + mac, + hostname: hostname ?? "lab-node", + role: validRole, + message: `PXE boot the machine to start installation (role=${validRole})`, + }); + }); + + // Receive install progress callbacks + app.post<{ + Body: { + mac?: string; + stage?: string; + detail?: string; + }; + }>("/api/progress", async (request, reply) => { + const { mac: rawMac, stage, detail } = request.body ?? {}; + const mac = (rawMac ?? "unknown").toLowerCase(); + const stageName = stage ?? "unknown"; + const detailStr = detail ?? ""; + + logger.info(`Progress: ${mac} ${stageName}${detailStr ? ` -- ${detailStr}` : ""}`); + + state.update((s) => { + const queueEntry = s.install_queue[mac]; + if (queueEntry) { + queueEntry.progress = stageName; + queueEntry.progress_at = new Date().toISOString(); + if (detailStr) { + queueEntry.progress_detail = detailStr; + } + + // Move to installed on completion + if (stageName === "complete") { + const cfg = s.install_queue[mac]; + delete s.install_queue[mac]; + + const ip = detailStr.startsWith("ready at ") + ? detailStr.replace("ready at ", "").trim() + : ""; + + const installedInfo: InstalledInfo = { + hostname: cfg?.hostname ?? "?", + role: cfg?.role ?? "?", + ip, + installed_at: new Date().toISOString(), + }; + s.installed[mac] = installedInfo; + + logger.info(`INSTALL COMPLETE: ${mac} -> ${installedInfo.hostname} (${ip})`); + } + } + }); + + return reply.send({ status: "ok" }); + }); + + // Receive discovery reports + app.post<{ + Body: { + mac?: string; + product?: string; + board?: string; + serial?: string; + manufacturer?: string; + cpu_model?: string; + cpu_cores?: number; + memory_gb?: number; + arch?: string; + disks?: Array<{ name: string; size_gb: number; model: string }>; + nics?: Array<{ name: string; mac: string; state: string }>; + }; + }>("/api/discover", async (request, reply) => { + const data = request.body; + if (!data) { + return reply.status(400).send({ error: "invalid JSON" }); + } + + const mac = (data.mac ?? "unknown").toLowerCase(); + const now = new Date().toISOString(); + + const isNew = state.load().discovered[mac] === undefined; + + state.update((s) => { + const existing = s.discovered[mac]; + const hwInfo: HardwareInfo = { + mac, + product: data.product ?? "unknown", + board: data.board ?? "unknown", + serial: data.serial ?? "unknown", + manufacturer: data.manufacturer ?? "unknown", + cpu_model: data.cpu_model ?? "unknown", + cpu_cores: data.cpu_cores ?? 0, + memory_gb: data.memory_gb ?? 0, + arch: data.arch ?? "unknown", + disks: data.disks ?? [], + nics: data.nics ?? [], + first_seen: existing?.first_seen ?? now, + last_seen: now, + }; + s.discovered[mac] = hwInfo; + }); + + const label = isNew ? "NEW MACHINE DISCOVERED" : "MACHINE RE-DISCOVERED"; + const cpu = data.cpu_model ?? "?"; + const cores = data.cpu_cores ?? "?"; + const mem = data.memory_gb ?? "?"; + logger.info(`${label}: ${mac} -- ${data.manufacturer ?? "?"} ${data.product ?? "?"} (${cpu}, ${cores} cores, ${mem}GB RAM)`); + + return reply.send({ status: "ok", mac, new: isNew }); + }); +} diff --git a/bastion/src/server/routes/dispatch.ts b/bastion/src/server/routes/dispatch.ts new file mode 100644 index 0000000..a8fd91a --- /dev/null +++ b/bastion/src/server/routes/dispatch.ts @@ -0,0 +1,64 @@ +// iPXE dispatch route. +// Routes PXE boot requests based on machine state: +// - install_queue -> install mode (serve Fedora installer + per-MAC kickstart) +// - installed -> exit (boot from local disk) +// - unknown -> discovery mode (collect hardware, POST to bastion) + +import type { FastifyInstance } from "fastify"; +import type { BastionConfig } from "../config.js"; +import type { StateManager } from "../services/state.js"; +import { + renderDiscoverIpxe, + renderInstallIpxe, + renderLocalBootIpxe, +} from "../../templates/boot.ipxe.js"; +import { logger } from "../services/logger.js"; + +export function registerDispatchRoutes( + app: FastifyInstance, + config: BastionConfig, + state: StateManager, +): void { + app.get<{ Querystring: { mac?: string } }>("/dispatch", async (request, reply) => { + const mac = (request.query.mac ?? "").toLowerCase().replace(/-/g, ":"); + const currentState = state.load(); + + const queueEntry = currentState.install_queue[mac]; + if (queueEntry) { + const hostname = queueEntry.hostname ?? "lab-node"; + logger.info(`INSTALL STARTED: ${mac} -> ${hostname}`); + + const script = renderInstallIpxe({ + mac, + hostname, + serverIp: config.serverIp, + httpPort: config.httpPort, + fedoraVersion: config.fedoraVersion, + fedoraMirror: config.fedoraMirror, + }); + + return reply.type("text/plain").send(script); + } + + const installedEntry = currentState.installed[mac]; + if (installedEntry) { + const hostname = installedEntry.hostname ?? "?"; + logger.info(`PXE request from ${mac} (${hostname}) - already installed, booting local disk`); + + const script = renderLocalBootIpxe(hostname); + return reply.type("text/plain").send(script); + } + + // Unknown MAC -> discovery mode + logger.info(`PXE request from ${mac} -> discovery mode`); + + const script = renderDiscoverIpxe({ + mac, + serverIp: config.serverIp, + httpPort: config.httpPort, + fedoraMirror: config.fedoraMirror, + }); + + return reply.type("text/plain").send(script); + }); +} diff --git a/bastion/src/server/routes/kickstart.ts b/bastion/src/server/routes/kickstart.ts new file mode 100644 index 0000000..dc43261 --- /dev/null +++ b/bastion/src/server/routes/kickstart.ts @@ -0,0 +1,34 @@ +// Kickstart generation routes. +// Serves per-MAC install kickstart and the static discovery kickstart. + +import type { FastifyInstance } from "fastify"; +import type { BastionConfig } from "../config.js"; +import type { StateManager } from "../services/state.js"; +import { generateInstallKickstart, generateDiscoverKickstart } from "../services/kickstart-generator.js"; + +export function registerKickstartRoutes( + app: FastifyInstance, + config: BastionConfig, + state: StateManager, +): void { + // Per-MAC install kickstart + app.get<{ Querystring: { mac?: string } }>("/ks", async (request, reply) => { + const mac = (request.query.mac ?? "").toLowerCase().replace(/-/g, ":"); + const currentState = state.load(); + const queueEntry = currentState.install_queue[mac]; + + const ks = generateInstallKickstart(config, { + hostname: queueEntry?.hostname ?? "lab-node", + disk: queueEntry?.disk ?? "", + role: queueEntry?.role ?? "worker", + }); + + return reply.type("text/plain").send(ks); + }); + + // Static discovery kickstart + app.get("/discover.ks", async (_request, reply) => { + const ks = generateDiscoverKickstart(config); + return reply.type("text/plain").send(ks); + }); +} diff --git a/bastion/src/server/server.ts b/bastion/src/server/server.ts new file mode 100644 index 0000000..d7d2215 --- /dev/null +++ b/bastion/src/server/server.ts @@ -0,0 +1,61 @@ +// Fastify application setup with all routes registered. + +import Fastify from "fastify"; +import fastifyStatic from "@fastify/static"; +import { mkdirSync, existsSync } from "node:fs"; +import type { BastionConfig } from "./config.js"; +import { StateManager } from "./services/state.js"; +import { logger } from "./services/logger.js"; +import { registerDispatchRoutes } from "./routes/dispatch.js"; +import { registerKickstartRoutes } from "./routes/kickstart.js"; +import { registerApiRoutes } from "./routes/api.js"; + +export function createApp(config: BastionConfig) { + const app = Fastify({ + logger: false, // We use winston instead + }); + + const state = new StateManager(config.stateFile); + state.init(); + + // Serve static files (vmlinuz, initrd.img, iPXE binaries) from the HTTP directory + mkdirSync(config.httpDir, { recursive: true }); + app.register(fastifyStatic, { + root: config.httpDir, + prefix: "/", + decorateReply: false, + }); + + // Also serve TFTP files (iPXE EFI binaries) over HTTP for UEFI HTTP Boot + if (existsSync(config.tftpDir)) { + app.register(fastifyStatic, { + root: config.tftpDir, + prefix: "/tftp/", + decorateReply: false, + }); + } + + // Register route handlers + registerDispatchRoutes(app, config, state); + registerKickstartRoutes(app, config, state); + registerApiRoutes(app, state); + + // Log all requests + app.addHook("onRequest", async (request) => { + logger.info(`HTTP: ${request.ip} ${request.method} ${request.url}`); + }); + + return { app, state }; +} + +export async function startServer(config: BastionConfig): Promise { + const { app } = createApp(config); + + try { + await app.listen({ port: config.httpPort, host: "0.0.0.0" }); + logger.info(`HTTP server listening on :${config.httpPort}`); + } catch (err) { + logger.error(`Failed to start HTTP server: ${err instanceof Error ? err.message : String(err)}`); + throw err; + } +} diff --git a/bastion/src/server/services/dnsmasq.ts b/bastion/src/server/services/dnsmasq.ts new file mode 100644 index 0000000..ce084fb --- /dev/null +++ b/bastion/src/server/services/dnsmasq.ts @@ -0,0 +1,70 @@ +// Generate dnsmasq configuration and manage the dnsmasq process lifecycle. + +import { writeFileSync, mkdirSync } from "node:fs"; +import { dirname } from "node:path"; +import type { ResultPromise } from "execa"; +import { execa } from "execa"; +import type { BastionConfig } from "../config.js"; +import { renderDnsmasqConf } from "../../templates/dnsmasq.conf.js"; +import { logger } from "./logger.js"; + +type DnsmasqProcess = ResultPromise<{ stdout: "pipe"; stderr: "pipe" }>; +let dnsmasqProcess: DnsmasqProcess | null = null; + +/** + * Generate the dnsmasq.conf file from the current configuration. + */ +export function generateDnsmasqConf(config: BastionConfig): string { + const confPath = `${config.bastionDir}/dnsmasq.conf`; + const content = renderDnsmasqConf(config); + mkdirSync(dirname(confPath), { recursive: true }); + writeFileSync(confPath, content); + logger.info(`Generated dnsmasq config: ${confPath}`); + return confPath; +} + +/** + * Start dnsmasq in the foreground as a child process. + */ +export async function startDnsmasq(config: BastionConfig): Promise { + const confPath = generateDnsmasqConf(config); + + logger.info(`Starting PXE server (${config.dhcpMode}DHCP on ${config.iface})...`); + + const proc = execa("dnsmasq", ["--no-daemon", `--conf-file=${confPath}`], { + stdout: "pipe", + stderr: "pipe", + }); + + dnsmasqProcess = proc; + + proc.stdout?.on("data", (data: Buffer) => { + const line = data.toString().trim(); + if (line) logger.info(`dnsmasq: ${line}`); + }); + + proc.stderr?.on("data", (data: Buffer) => { + const line = data.toString().trim(); + if (line) logger.info(`dnsmasq: ${line}`); + }); + + proc.on("exit", (code) => { + if (code !== null && code !== 0) { + logger.error(`dnsmasq exited with code ${code}. Check if another DHCP/TFTP service is running.`); + } + dnsmasqProcess = null; + }); + + return proc; +} + +/** + * Stop the running dnsmasq process. + */ +export function stopDnsmasq(): void { + if (dnsmasqProcess) { + logger.info("Stopping dnsmasq..."); + dnsmasqProcess.kill("SIGTERM"); + dnsmasqProcess = null; + } +} diff --git a/bastion/src/server/services/kickstart-generator.ts b/bastion/src/server/services/kickstart-generator.ts new file mode 100644 index 0000000..821f9df --- /dev/null +++ b/bastion/src/server/services/kickstart-generator.ts @@ -0,0 +1,44 @@ +// Generate kickstart content for discovery and install modes. +// Uses template literal functions -- no external template engine. + +import type { BastionConfig } from "../config.js"; +import { renderDiscoverKickstart } from "../../templates/discover.ks.js"; +import { renderInstallKickstart, type InstallKickstartParams } from "../../templates/install.ks.js"; + +/** + * Generate a discovery kickstart that collects hardware info and POSTs to bastion. + */ +export function generateDiscoverKickstart(config: BastionConfig): string { + return renderDiscoverKickstart({ + serverIp: config.serverIp, + httpPort: config.httpPort, + }); +} + +/** + * Generate an install kickstart with LVM partitioning, packages, and post-install configuration. + */ +export function generateInstallKickstart( + config: BastionConfig, + params: { + hostname: string; + disk: string; + role: "worker" | "infra"; + }, +): string { + const ksParams: InstallKickstartParams = { + hostname: params.hostname, + disk: params.disk, + role: params.role, + domain: config.domain, + fedoraVersion: config.fedoraVersion, + timezone: config.timezone, + locale: config.locale, + serverIp: config.serverIp, + httpPort: config.httpPort, + sshKeys: config.sshKeys, + adminUser: config.adminUser, + }; + + return renderInstallKickstart(ksParams); +} diff --git a/bastion/src/server/services/logger.ts b/bastion/src/server/services/logger.ts new file mode 100644 index 0000000..7f80de9 --- /dev/null +++ b/bastion/src/server/services/logger.ts @@ -0,0 +1,17 @@ +// Winston logger instance shared across the bastion application. + +import winston from "winston"; + +export const logger = winston.createLogger({ + level: "info", + format: winston.format.combine( + winston.format.timestamp({ format: "HH:mm:ss" }), + winston.format.printf(({ timestamp, level, message }) => { + const prefix = level === "error" ? "\x1b[31m[bastion]\x1b[0m" + : level === "warn" ? "\x1b[33m[bastion]\x1b[0m" + : "\x1b[32m[bastion]\x1b[0m"; + return `${prefix} ${timestamp as string} ${message as string}`; + }), + ), + transports: [new winston.transports.Console()], +}); diff --git a/bastion/src/server/services/network.ts b/bastion/src/server/services/network.ts new file mode 100644 index 0000000..8eafda2 --- /dev/null +++ b/bastion/src/server/services/network.ts @@ -0,0 +1,158 @@ +// Auto-detect network interface, IP, gateway, SSH keys, and admin user. + +import { execSync } from "node:child_process"; +import { readFileSync, existsSync, mkdirSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import type { BastionConfig } from "../config.js"; +import { logger } from "./logger.js"; + +/** + * Detect the default network interface from the routing table. + */ +export function detectInterface(): string { + const output = execSync("ip route", { encoding: "utf-8" }); + const match = output.match(/default\s+.*\s+dev\s+(\S+)/); + if (!match?.[1]) { + throw new Error("Cannot detect default network interface"); + } + return match[1]; +} + +/** + * Detect the IPv4 address on a given interface. + */ +export function detectIp(iface: string): string { + const output = execSync(`ip -4 addr show ${iface}`, { encoding: "utf-8" }); + const match = output.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/); + if (!match?.[1]) { + throw new Error(`Cannot detect IP on interface ${iface}`); + } + return match[1]; +} + +/** + * Derive the /24 network address from an IP. + */ +export function deriveNetwork(ip: string): string { + const parts = ip.split("."); + return `${parts[0]}.${parts[1]}.${parts[2]}.0`; +} + +/** + * Detect the default gateway. + */ +export function detectGateway(): string { + const output = execSync("ip route", { encoding: "utf-8" }); + const match = output.match(/default\s+via\s+(\S+)/); + if (!match?.[1]) { + throw new Error("Cannot detect default gateway"); + } + return match[1]; +} + +/** + * Collect SSH public keys from the current user's SSH directory. + * Sources: authorized_keys, then id_ed25519.pub, id_rsa.pub, id_ecdsa.pub (deduplicated). + */ +export function collectSshKeys(bastionDir: string): { keys: string[]; source: string } { + const realHome = process.env["SUDO_USER"] + ? execSync(`getent passwd ${process.env["SUDO_USER"]}`, { encoding: "utf-8" }) + .split(":")[5] + ?.trim() ?? homedir() + : homedir(); + + const keys: string[] = []; + const fingerprints = new Set(); + let source = ""; + + // Read authorized_keys + const authKeysPath = join(realHome, ".ssh", "authorized_keys"); + if (existsSync(authKeysPath)) { + const content = readFileSync(authKeysPath, "utf-8"); + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith("#")) { + const fp = trimmed.split(/\s+/)[1]; + if (fp && !fingerprints.has(fp)) { + keys.push(trimmed); + fingerprints.add(fp); + } + } + } + source = authKeysPath; + } + + // Also include local pubkey files + const pubKeyFiles = ["id_ed25519.pub", "id_rsa.pub", "id_ecdsa.pub"]; + for (const keyFile of pubKeyFiles) { + const keyPath = join(realHome, ".ssh", keyFile); + if (existsSync(keyPath)) { + const keyData = readFileSync(keyPath, "utf-8").trim(); + const fp = keyData.split(/\s+/)[1]; + if (fp && !fingerprints.has(fp)) { + keys.push(keyData); + fingerprints.add(fp); + source = source ? `${source} + ${keyPath}` : keyPath; + } + } + } + + // Generate a keypair if no keys found + if (keys.length === 0) { + const generatedKey = join(bastionDir, "bastion_ed25519"); + if (!existsSync(generatedKey)) { + mkdirSync(bastionDir, { recursive: true }); + logger.warn("No SSH keys found -- generating ed25519 keypair..."); + execSync(`ssh-keygen -t ed25519 -f "${generatedKey}" -N "" -C "bastion-generated@$(hostname)"`, { + encoding: "utf-8", + stdio: "pipe", + }); + } + const pubKey = readFileSync(`${generatedKey}.pub`, "utf-8").trim(); + keys.push(pubKey); + source = `${generatedKey} (generated)`; + logger.warn(`Using generated keypair: ${generatedKey}`); + logger.warn("Save this private key -- it is the only way to access installed machines."); + } + + return { keys, source }; +} + +/** + * Detect the admin username (SUDO_USER or current user, excluding root). + */ +export function detectAdminUser(): string { + const user = process.env["SUDO_USER"] ?? process.env["USER"] ?? ""; + return user === "root" ? "" : user; +} + +/** + * Populate runtime network config fields on the config object. + */ +export function populateNetworkConfig(config: BastionConfig): BastionConfig { + const iface = config.iface || detectInterface(); + const serverIp = config.serverIp || detectIp(iface); + const network = config.network || deriveNetwork(serverIp); + const gateway = config.gateway || detectGateway(); + const { keys: sshKeys, source: sshSource } = config.sshKeys.length > 0 + ? { keys: config.sshKeys, source: "config" } + : collectSshKeys(config.bastionDir); + const adminUser = config.adminUser || detectAdminUser(); + + logger.info(`Interface: ${iface} IP: ${serverIp} Network: ${network}`); + logger.info(`SSH keys: ${sshKeys.length} key(s) from ${sshSource}`); + if (adminUser) { + logger.info(`Admin user: ${adminUser} (will be created on installed machines)`); + } + + return { + ...config, + iface, + serverIp, + network, + gateway, + sshKeys, + adminUser, + }; +} diff --git a/bastion/src/server/services/state.ts b/bastion/src/server/services/state.ts new file mode 100644 index 0000000..b48091f --- /dev/null +++ b/bastion/src/server/services/state.ts @@ -0,0 +1,92 @@ +// JSON file-backed state management for discovered machines, install queue, and installed machines. + +import { readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs"; +import { dirname } from "node:path"; + +export interface HardwareInfo { + mac: string; + product: string; + board: string; + serial: string; + manufacturer: string; + cpu_model: string; + cpu_cores: number; + memory_gb: number; + arch: string; + disks: Array<{ name: string; size_gb: number; model: string }>; + nics: Array<{ name: string; mac: string; state: string }>; + first_seen: string; + last_seen: string; +} + +export interface InstallConfig { + hostname: string; + disk: string; + role: "worker" | "infra"; + queued_at: string; + progress?: string; + progress_at?: string; + progress_detail?: string; +} + +export interface InstalledInfo { + hostname: string; + role: string; + ip: string; + installed_at: string; +} + +export interface BastionState { + discovered: Record; + install_queue: Record; + installed: Record; +} + +const EMPTY_STATE: BastionState = { + discovered: {}, + install_queue: {}, + installed: {}, +}; + +export class StateManager { + constructor(private readonly stateFile: string) {} + + load(): BastionState { + try { + const raw = readFileSync(this.stateFile, "utf-8"); + const parsed = JSON.parse(raw) as Partial; + return { + discovered: parsed.discovered ?? {}, + install_queue: parsed.install_queue ?? {}, + installed: parsed.installed ?? {}, + }; + } catch { + return { ...EMPTY_STATE }; + } + } + + save(state: BastionState): void { + mkdirSync(dirname(this.stateFile), { recursive: true }); + const tmp = `${this.stateFile}.tmp`; + writeFileSync(tmp, JSON.stringify(state, null, 2)); + renameSync(tmp, this.stateFile); + } + + init(): void { + try { + readFileSync(this.stateFile, "utf-8"); + } catch { + this.save({ ...EMPTY_STATE }); + } + } + + /** + * Atomically read, modify, and write state. + */ + update(fn: (state: BastionState) => void): BastionState { + const state = this.load(); + fn(state); + this.save(state); + return state; + } +} diff --git a/bastion/src/templates/boot.ipxe.ts b/bastion/src/templates/boot.ipxe.ts new file mode 100644 index 0000000..565b7ea --- /dev/null +++ b/bastion/src/templates/boot.ipxe.ts @@ -0,0 +1,93 @@ +// iPXE boot script templates for dispatch routing. + +export interface BootIpxeParams { + serverIp: string; + httpPort: number; +} + +/** + * Initial iPXE boot script that chains to the dispatch endpoint. + * This is what dnsmasq serves to iPXE clients via HTTP. + */ +export function renderBootIpxe(params: BootIpxeParams): string { + return `#!ipxe + +echo +echo ============================================ +echo Lab PXE Bastion +echo Contacting server for instructions... +echo ============================================ +echo + +chain http://${params.serverIp}:${params.httpPort}/dispatch?mac=\${net0/mac} +`; +} + +/** + * iPXE script for discovery mode -- boots Fedora installer with discovery kickstart. + */ +export function renderDiscoverIpxe(params: { + mac: string; + serverIp: string; + httpPort: number; + fedoraMirror: string; +}): string { + return `#!ipxe + +echo +echo ============================================= +echo Lab PXE Bastion - DISCOVERY MODE +echo MAC: ${params.mac} +echo Collecting hardware info... +echo ============================================= +echo + +kernel http://${params.serverIp}:${params.httpPort}/vmlinuz inst.ks=http://${params.serverIp}:${params.httpPort}/discover.ks inst.stage2=${params.fedoraMirror} inst.text +initrd http://${params.serverIp}:${params.httpPort}/initrd.img +boot +`; +} + +/** + * iPXE script for install mode -- boots Fedora installer with per-MAC kickstart. + */ +export function renderInstallIpxe(params: { + mac: string; + hostname: string; + serverIp: string; + httpPort: number; + fedoraVersion: string; + fedoraMirror: string; +}): string { + return `#!ipxe + +echo +echo ============================================= +echo Lab PXE Bastion - INSTALLING Fedora ${params.fedoraVersion} +echo Target: ${params.hostname} +echo MAC: ${params.mac} +echo ============================================= +echo + +kernel http://${params.serverIp}:${params.httpPort}/vmlinuz inst.ks=http://${params.serverIp}:${params.httpPort}/ks?mac=${params.mac} inst.repo=${params.fedoraMirror} inst.text +initrd http://${params.serverIp}:${params.httpPort}/initrd.img +boot +`; +} + +/** + * iPXE script for already-installed machines -- exits to boot from local disk. + */ +export function renderLocalBootIpxe(hostname: string): string { + return `#!ipxe + +echo +echo ============================================= +echo Lab PXE Bastion - ${hostname} +echo Already installed, booting from local disk +echo ============================================= +echo +sleep 3 +exit +`; +} diff --git a/bastion/src/templates/discover.ks.ts b/bastion/src/templates/discover.ks.ts new file mode 100644 index 0000000..79497ba --- /dev/null +++ b/bastion/src/templates/discover.ks.ts @@ -0,0 +1,118 @@ +// Discovery kickstart template. +// Boots Fedora installer, collects hardware info, POSTs to bastion, reboots. +// Never touches the disk. + +export interface DiscoverKickstartParams { + serverIp: string; + httpPort: number; +} + +export function renderDiscoverKickstart(params: DiscoverKickstartParams): string { + const bastionUrl = `http://${params.serverIp}:${params.httpPort}`; + + return `# Lab Bastion -- Discovery Mode +# Collects hardware inventory and reboots. Does NOT install anything. + +%pre --erroronfail --log=/tmp/discover.log +#!/bin/bash +set -x + +# -- Collect hardware info from /proc, /sys, and available tools -- + +MAC=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}') +PRODUCT=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "unknown") +BOARD=$(cat /sys/class/dmi/id/board_name 2>/dev/null || echo "unknown") +SERIAL=$(cat /sys/class/dmi/id/product_serial 2>/dev/null || echo "unknown") +MANUFACTURER=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "unknown") +CPUMODEL=$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | sed 's/^ //') +CPUCORES=$(grep -c '^processor' /proc/cpuinfo) +MEMGB=$(awk '/MemTotal/ {printf "%d", $2/1024/1024}' /proc/meminfo) +ARCHTYPE=$(uname -m) + +# Disk info +DISKS_JSON=$(lsblk -Jb -o NAME,SIZE,TYPE,MODEL 2>/dev/null | python3 -c " +import sys, json +data = json.load(sys.stdin) +disks = [d for d in data.get('blockdevices', []) if d.get('type') == 'disk'] +result = [] +for d in disks: + size_gb = round(int(d.get('size', 0)) / 1073741824, 1) + result.append({ + 'name': d.get('name', '?'), + 'size_gb': size_gb, + 'model': (d.get('model') or 'unknown').strip() + }) +print(json.dumps(result)) +" 2>/dev/null || echo '[]') + +# Network interfaces +NICS_JSON=$(ip -j link show 2>/dev/null | python3 -c " +import sys, json +nics = json.load(sys.stdin) +result = [] +for n in nics: + if n.get('link_type') == 'loopback': + continue + result.append({ + 'name': n.get('ifname', '?'), + 'mac': n.get('address', '?'), + 'state': n.get('operstate', '?') + }) +print(json.dumps(result)) +" 2>/dev/null || echo '[]') + +# -- Build and POST discovery payload -- + +PAYLOAD=$(python3 -c " +import json +print(json.dumps({ + 'mac': '$MAC', + 'product': '$PRODUCT', + 'board': '$BOARD', + 'serial': '$SERIAL', + 'manufacturer': '$MANUFACTURER', + 'cpu_model': '$CPUMODEL', + 'cpu_cores': int('$CPUCORES' or 0), + 'memory_gb': int('$MEMGB' or 0), + 'arch': '$ARCHTYPE', + 'disks': $DISKS_JSON, + 'nics': $NICS_JSON +})) +") + +# POST to bastion +BASTION_URL="${bastionUrl}/api/discover" + +if command -v curl >/dev/null 2>&1; then + curl -sf -X POST "$BASTION_URL" \\ + -H "Content-Type: application/json" \\ + -d "$PAYLOAD" || true +else + python3 -c " +import urllib.request +req = urllib.request.Request('$BASTION_URL', + data=b'''$PAYLOAD''', + headers={'Content-Type': 'application/json'}) +try: + urllib.request.urlopen(req, timeout=10) +except Exception as e: + print(f'POST failed: {e}') +" +fi + +# -- Reboot -- do NOT let Anaconda proceed -- +echo "" +echo "=== Discovery complete, rebooting ===" +echo "" +sleep 3 +echo 1 > /proc/sys/kernel/sysrq +echo b > /proc/sysrq-trigger +sleep 5 +reboot -f + +%end + +# Anaconda should never get here, but just in case: +reboot +`; +} diff --git a/bastion/src/templates/dnsmasq.conf.ts b/bastion/src/templates/dnsmasq.conf.ts new file mode 100644 index 0000000..f40c15d --- /dev/null +++ b/bastion/src/templates/dnsmasq.conf.ts @@ -0,0 +1,88 @@ +// dnsmasq configuration template. +// Supports proxy DHCP mode (alongside existing DHCP) and full DHCP mode. +// Handles UEFI HTTP Boot, iPXE chainloading, and PXE service directives. + +import type { BastionConfig } from "../server/config.js"; + +export function renderDnsmasqConf(config: BastionConfig): string { + const { + iface, + serverIp, + httpPort, + network, + gateway, + dhcpMode, + tftpDir, + } = config; + + // Derive DHCP range for full mode + let dhcpRangeStart = config.dhcpRangeStart; + let dhcpRangeEnd = config.dhcpRangeEnd; + if (dhcpMode === "full") { + const networkBase = network.replace(/\.0$/, ""); + dhcpRangeStart = dhcpRangeStart || `${networkBase}.100`; + dhcpRangeEnd = dhcpRangeEnd || `${networkBase}.200`; + } + + const dhcpSection = dhcpMode === "full" + ? `# Full DHCP mode -- bastion is the only DHCP server on this network +dhcp-range=${dhcpRangeStart},${dhcpRangeEnd},255.255.255.0,12h +dhcp-option=3,${gateway} +dhcp-option=6,${gateway}` + : `# ProxyDHCP -- works alongside existing DHCP (UniFi etc) +dhcp-range=${network},proxy`; + + return `# Lab PXE Bastion -- dnsmasq config + +# Disable DNS (we only want DHCP/TFTP) +port=0 + +# Listen on the right interface +interface=${iface} +bind-dynamic + +${dhcpSection} + +# TFTP for initial PXE boot +enable-tftp +tftp-root=${tftpDir} +tftp-no-blocksize + +# Detect client architecture -- PXE (TFTP) clients +dhcp-match=set:bios,option:client-arch,0 +dhcp-match=set:efi-x86_64,option:client-arch,7 +dhcp-match=set:efi-x86_64,option:client-arch,9 +dhcp-match=set:efi-arm64,option:client-arch,11 + +# Detect client architecture -- UEFI HTTP Boot clients (no TFTP size limit) +dhcp-match=set:httpboot-x86_64,option:client-arch,16 +dhcp-match=set:httpboot-arm64,option:client-arch,20 + +# Detect iPXE clients (already chainloaded) +dhcp-userclass=set:ipxe,iPXE + +# UEFI HTTP Boot -> serve full iPXE EFI via HTTP (no TFTP size limit) +dhcp-boot=tag:httpboot-x86_64,http://${serverIp}:${httpPort}/ipxe-real.efi +dhcp-boot=tag:httpboot-arm64,http://${serverIp}:${httpPort}/ipxe-arm64.efi +# Echo vendor class back to HTTP Boot clients (required by UEFI HTTP Boot spec) +dhcp-option-force=tag:httpboot-x86_64,60,HTTPClient +dhcp-option-force=tag:httpboot-arm64,60,HTTPClient + +# First PXE boot -> serve iPXE binary via TFTP (BIOS and UEFI fallback) +dhcp-boot=tag:bios,tag:!ipxe,undionly.kpxe +dhcp-boot=tag:efi-x86_64,tag:!ipxe,ipxe.efi +dhcp-boot=tag:efi-arm64,tag:!ipxe,ipxe-arm64.efi + +# iPXE clients -> chain to boot script via HTTP +dhcp-boot=tag:ipxe,http://${serverIp}:${httpPort}/boot.ipxe + +# PXE service directives (needed for proxy DHCP to respond properly) +pxe-service=tag:!ipxe,x86PC,"PXE Boot",undionly.kpxe +pxe-service=tag:!ipxe,X86-64_EFI,"PXE Boot",ipxe.efi +pxe-service=tag:!ipxe,BC_EFI,"PXE Boot",ipxe.efi +pxe-service=tag:!ipxe,ARM64_EFI,"PXE Boot",ipxe-arm64.efi + +# Verbose logging +log-dhcp +`; +} diff --git a/bastion/src/templates/install.ks.ts b/bastion/src/templates/install.ks.ts new file mode 100644 index 0000000..dba5012 --- /dev/null +++ b/bastion/src/templates/install.ks.ts @@ -0,0 +1,365 @@ +// Install kickstart template. +// Full Fedora server install with LVM partitioning, %pre for reprovision detection, +// packages, and %post with SSH keys, user creation, k3s prereqs, progress callbacks. + +export interface InstallKickstartParams { + hostname: string; + disk: string; + role: "worker" | "infra"; + domain: string; + fedoraVersion: string; + timezone: string; + locale: string; + serverIp: string; + httpPort: number; + sshKeys: string[]; + adminUser: string; +} + +export function renderInstallKickstart(params: InstallKickstartParams): string { + const { + hostname, + disk, + role, + domain, + fedoraVersion, + timezone, + locale, + serverIp, + httpPort, + sshKeys, + adminUser, + } = params; + + const fqdn = domain ? `${hostname}.${domain}` : hostname; + const vg = "labvg"; + const now = new Date().toISOString(); + const hasLonghorn = role === "worker"; + + // -- Auth section -- + const auth = sshKeys.length > 0 + ? `rootpw --lock\nsshkey --username=root "${sshKeys[0]}"` + : "rootpw --plaintext changeme"; + + // -- Admin user directive -- + const userDirective = adminUser + ? `user --name=${adminUser} --groups=wheel --lock` + : ""; + + // -- SSH keys for %post -- + const allKeys = sshKeys.join("\n"); + let sshPostBlock = ""; + if (sshKeys.length > 0) { + sshPostBlock = ` +# Set up SSH keys for root +mkdir -p /root/.ssh && chmod 700 /root/.ssh +cat > /root/.ssh/authorized_keys << 'SSHKEYS' +${allKeys} +SSHKEYS +chmod 600 /root/.ssh/authorized_keys`; + } + + if (adminUser && sshKeys.length > 0) { + sshPostBlock += ` + +# Set up SSH keys for ${adminUser} +ADMIN_HOME=$(getent passwd ${adminUser} | cut -d: -f6) +mkdir -p "$ADMIN_HOME/.ssh" && chmod 700 "$ADMIN_HOME/.ssh" +cp /root/.ssh/authorized_keys "$ADMIN_HOME/.ssh/authorized_keys" +chown -R ${adminUser}:${adminUser} "$ADMIN_HOME/.ssh" +chmod 600 "$ADMIN_HOME/.ssh/authorized_keys" + +# Fix SELinux contexts for SSH +restorecon -R /root/.ssh "$ADMIN_HOME/.ssh" 2>/dev/null || true + +# Passwordless sudo for ${adminUser} +echo '${adminUser} ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/${adminUser} +chmod 440 /etc/sudoers.d/${adminUser}`; + } + + // -- Disk detection -- + const diskLine = disk + ? `DISK="${disk}"` + : `DISK="" +for d in /dev/nvme0n1 /dev/sda /dev/vda; do + [ -b "$d" ] && { DISK="$(basename $d)"; break; } +done +[ -z "$DISK" ] && { echo "ERROR: no disk found"; exit 1; }`; + + // -- Longhorn LV for fresh install -- + const longhornFreshLine = hasLonghorn + ? `logvol /var/lib/longhorn --vgname=${vg} --name=longhorn --fstype=xfs --grow --size=1` + : ""; + + return `# Lab Bastion -- Fedora ${fedoraVersion} server install +# Generated: ${now} +# Target: ${fqdn} (role=${role}) + +text +reboot + +lang ${locale} +keyboard uk +timezone ${timezone} --utc + +network --bootproto=dhcp --activate --hostname=${fqdn} + +${auth} +${userDirective} + +bootloader --append="console=tty0 console=ttyS0,115200n8" + +url --mirrorlist=https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-$releasever&arch=$basearch + +# Partitioning is generated dynamically by %pre (supports reprovision preservation) +%include /tmp/part.ks + +%pre --log=/tmp/pre-partition.log +#!/bin/bash +set -x + +# Progress callback helper +bastion_progress() { + local stage="$1" detail="\${2:-}" + local mac=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}') + curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" \\ + -H "Content-Type: application/json" \\ + -d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true +} + +bastion_progress "partitioning" "preparing disk layout" + +VG="${vg}" +${diskLine} + +REPROVISION=no + +# Check if VG exists (reprovision scenario) +if vgs $VG &>/dev/null; then + echo "=== Existing VG found - reprovision mode ===" + REPROVISION=yes + + # Detect which data LVs to preserve + PRESERVE_LONGHORN=no; PRESERVE_SRV=no; PRESERVE_HOME=no + lvs $VG/longhorn &>/dev/null && PRESERVE_LONGHORN=yes + lvs $VG/srv &>/dev/null && PRESERVE_SRV=yes + lvs $VG/home &>/dev/null && PRESERVE_HOME=yes + + echo "Preserving: longhorn=$PRESERVE_LONGHORN srv=$PRESERVE_SRV home=$PRESERVE_HOME" + + # Remove only OS logical volumes (keep data LVs) + for lv in root var varlog swap; do + lvremove -f $VG/$lv 2>/dev/null || true + done +fi + +if [ "$REPROVISION" = "yes" ]; then + # Find existing boot partitions by type + EFI_PART=$(blkid -t TYPE=vfat -o device /dev/\${DISK}* 2>/dev/null | head -1) + BOOT_PART=$(blkid -t TYPE=ext4 -o device /dev/\${DISK}* 2>/dev/null | head -1) + EFI_PART=\${EFI_PART:-/dev/\${DISK}1} + BOOT_PART=\${BOOT_PART:-/dev/\${DISK}2} + echo "Reusing EFI=$EFI_PART BOOT=$BOOT_PART" + + # Build partition config reusing existing PV/VG + cat > /tmp/part.ks << PARTEOF +ignoredisk --only-use=$DISK +clearpart --none +part /boot/efi --onpart=$EFI_PART --fstype=efi +part /boot --onpart=$BOOT_PART --fstype=ext4 +volgroup ${vg} --useexisting --noformat +logvol swap --vgname=${vg} --name=swap --fstype=swap --size=27648 +logvol / --vgname=${vg} --name=root --fstype=xfs --size=33792 +logvol /var --vgname=${vg} --name=var --fstype=xfs --size=102400 +logvol /var/log --vgname=${vg} --name=varlog --fstype=xfs --size=10240 +PARTEOF + + # Preserve or recreate data LVs + if [ "$PRESERVE_HOME" = "yes" ]; then + echo "logvol /home --vgname=${vg} --name=home --useexisting --noformat" >> /tmp/part.ks + else + echo "logvol /home --vgname=${vg} --name=home --fstype=xfs --size=10240" >> /tmp/part.ks + fi + + if [ "$PRESERVE_SRV" = "yes" ]; then + echo "logvol /srv --vgname=${vg} --name=srv --useexisting --noformat" >> /tmp/part.ks + else + echo "logvol /srv --vgname=${vg} --name=srv --fstype=xfs --size=20480" >> /tmp/part.ks + fi + + if [ "$PRESERVE_LONGHORN" = "yes" ]; then + echo "logvol /var/lib/longhorn --vgname=${vg} --name=longhorn --useexisting --noformat" >> /tmp/part.ks + fi + +else + # Fresh install + cat > /tmp/part.ks << PARTEOF +ignoredisk --only-use=$DISK +clearpart --all --initlabel --drives=$DISK +part /boot/efi --fstype=efi --size=600 --ondisk=$DISK +part /boot --fstype=ext4 --size=3072 --ondisk=$DISK +part pv.01 --size=1 --grow --ondisk=$DISK +volgroup ${vg} pv.01 +logvol swap --vgname=${vg} --name=swap --fstype=swap --size=27648 +logvol / --vgname=${vg} --name=root --fstype=xfs --size=33792 +logvol /var --vgname=${vg} --name=var --fstype=xfs --size=102400 +logvol /var/log --vgname=${vg} --name=varlog --fstype=xfs --size=10240 +logvol /home --vgname=${vg} --name=home --fstype=xfs --size=10240 +logvol /srv --vgname=${vg} --name=srv --fstype=xfs --size=20480 +${longhornFreshLine} +PARTEOF +fi + +echo "=== Generated partition config ===" +cat /tmp/part.ks +echo "===================================" + +bastion_progress "partitioning" "layout ready, starting install" + +%end + +%packages +@core +openssh-server +vim-enhanced +tmux +git +curl +wget +python3 +lshw +dmidecode +dnf-plugins-core + +# Networking and diagnostics +NetworkManager +bind-utils +net-tools +iproute +iputils +traceroute +tcpdump +htop +iotop +strace +jq + +# k3s prerequisites +container-selinux +iptables-nft +nftables +policycoreutils-python-utils +chrony +tar +socat +conntrack-tools +ethtool + +# Boot management +efibootmgr + +# Puppet prerequisites +ruby +ruby-libs + +# Exclude desktop +-@workstation-product +-@gnome-desktop +-gnome-shell +-gdm +-PackageKit +-PackageKit-glib +%end + +%post --log=/root/bastion-post-install.log +#!/bin/bash +set -x + +# Progress callback helper +bastion_progress() { + local stage="$1" detail="\${2:-}" + local mac=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}') + curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" \\ + -H "Content-Type: application/json" \\ + -d "{\\"mac\\":\\"$mac\\",\\"stage\\":\\"$stage\\",\\"detail\\":\\"$detail\\"}" 2>/dev/null || true +} + +bastion_progress "post-install" "configuring system" + +# -- SSH -- +systemctl enable --now sshd +sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config +sed -i 's/^#\\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config +${sshPostBlock} + +# -- Hostname and domain -- +hostnamectl set-hostname ${fqdn} + +# -- tmpfs for /tmp -- +echo "tmpfs /tmp tmpfs defaults,noatime,nosuid,nodev,size=4G 0 0" >> /etc/fstab + +# -- Kernel modules for k3s -- +cat > /etc/modules-load.d/k3s.conf << 'MODULES' +br_netfilter +overlay +ip_conntrack +MODULES +modprobe br_netfilter || true +modprobe overlay || true + +# -- Sysctl for k3s networking -- +cat > /etc/sysctl.d/90-k3s.conf << 'SYSCTL' +net.bridge.bridge-nf-call-iptables = 1 +net.bridge.bridge-nf-call-ip6tables = 1 +net.ipv4.ip_forward = 1 +net.ipv6.conf.all.forwarding = 1 +fs.inotify.max_user_instances = 524288 +fs.inotify.max_user_watches = 1048576 +SYSCTL +sysctl --system || true + +# -- Disable firewalld (k3s manages its own iptables rules) -- +systemctl disable --now firewalld || true + +# -- Enable chronyd for time sync -- +systemctl enable --now chronyd + +# -- Set boot order: local disk first, PXE after -- +if command -v efibootmgr >/dev/null 2>&1; then + FEDORA_ENTRY=$(efibootmgr | grep -i fedora | head -1 | grep -oP 'Boot\\K[0-9A-F]+') + if [ -n "$FEDORA_ENTRY" ]; then + CURRENT_ORDER=$(efibootmgr | grep BootOrder | cut -d: -f2 | tr -d ' ') + NEW_ORDER="$FEDORA_ENTRY,$(echo "$CURRENT_ORDER" | sed "s/$FEDORA_ENTRY,\\\\?//;s/,$//")" + efibootmgr -o "$NEW_ORDER" || true + echo "Boot order set: Fedora first ($NEW_ORDER)" + fi +fi + +# -- Provisioning metadata -- +cat > /etc/lab-provisioned << PROVEOF +hostname: ${fqdn} +role: ${role} +provisioned: $(date -Iseconds) +bastion: ${serverIp} +PROVEOF + +cat > /root/README << 'README' +# Lab Node -- ${fqdn} (role: ${role}) +# +# Next steps: +# 1. Install puppet agent: +# dnf install -y puppet-agent +# +# 2. Install k3s: +# curl -sfL https://get.k3s.io | sh - +# +# 3. Or join existing cluster: +# curl -sfL https://get.k3s.io | K3S_URL=https://:6443 K3S_TOKEN= sh - +README + +IP_ADDR=$(ip -4 addr show | awk '/inet / && !/127.0.0/ {split($2,a,"/"); print a[1]; exit}') +bastion_progress "complete" "ready at $IP_ADDR" + +%end +`; +} diff --git a/bastion/stack/.env.example b/bastion/stack/.env.example new file mode 100644 index 0000000..c968d21 --- /dev/null +++ b/bastion/stack/.env.example @@ -0,0 +1,33 @@ +# Lab PXE Bastion -- Environment Configuration +# +# Copy this file to .env and adjust as needed. + +# Fedora version to install +FEDORA_VERSION=43 + +# Target architecture +ARCH=x86_64 + +# HTTP server port +HTTP_PORT=8080 + +# System locale and timezone for installed machines +TIMEZONE=Europe/London +LOCALE=en_GB.UTF-8 + +# Data directory (inside container) +BASTION_DIR=/data + +# Internal domain for hostnames (e.g., node1.ad.itaz.eu) +DOMAIN=ad.itaz.eu + +# DHCP mode: "proxy" works alongside existing DHCP (e.g., UniFi) +# "full" means bastion is the only DHCP server +DHCP_MODE=proxy + +# Only used in full DHCP mode -- auto-derived from network if empty +DHCP_RANGE_START= +DHCP_RANGE_END= + +# Path to SSH keys directory on host (mounted read-only) +SSH_KEY_PATH=~/.ssh diff --git a/bastion/stack/Dockerfile b/bastion/stack/Dockerfile new file mode 100644 index 0000000..3800fd8 --- /dev/null +++ b/bastion/stack/Dockerfile @@ -0,0 +1,37 @@ +FROM fedora:43 + +# Install system dependencies +RUN dnf install -y \ + dnsmasq \ + ipxe-bootimgs-x86 \ + ipxe-bootimgs-aarch64 \ + curl \ + openssh-clients \ + && dnf clean all + +# Install Node.js 22 +RUN dnf install -y nodejs npm && dnf clean all +RUN npm install -g pnpm@9 + +# Create app directory +WORKDIR /app + +# Copy package files and install dependencies +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install + +# Copy built application +COPY dist/ ./dist/ + +# Create data directories +RUN mkdir -p /data/state /data/tftp /data/http + +ENV BASTION_DIR=/data +ENV HTTP_PORT=8080 + +EXPOSE 8080/tcp +EXPOSE 67/udp +EXPOSE 69/udp +EXPOSE 4011/udp + +ENTRYPOINT ["node", "dist/cli/index.js", "serve"] diff --git a/bastion/stack/docker-compose.yml b/bastion/stack/docker-compose.yml new file mode 100644 index 0000000..ce07372 --- /dev/null +++ b/bastion/stack/docker-compose.yml @@ -0,0 +1,21 @@ +services: + bastion: + build: + context: .. + dockerfile: stack/Dockerfile + network_mode: host + restart: unless-stopped + env_file: .env + volumes: + - bastion-state:/data/state + - bastion-tftp:/data/tftp + - bastion-http:/data/http + - ${SSH_KEY_PATH:-~/.ssh}:/root/.ssh:ro + cap_add: + - NET_ADMIN + - NET_RAW + +volumes: + bastion-state: + bastion-tftp: + bastion-http: diff --git a/bastion/tsconfig.json b/bastion/tsconfig.json new file mode 100644 index 0000000..6e4c9ce --- /dev/null +++ b/bastion/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true, + "incremental": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "isolatedModules": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +}